diff --git a/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs b/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs index f8c74f3f8f..a1fee1a2e3 100644 --- a/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs +++ b/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; using System.Runtime.InteropServices; @@ -16,7 +17,7 @@ namespace Perspex.Cairo.Media /// /// Draws using Direct2D1. /// - public class DrawingContext : IDrawingContext, IDisposable + public class DrawingContext : IDrawingContextImpl, IDisposable { /// /// The cairo context. @@ -30,7 +31,6 @@ namespace Perspex.Cairo.Media public DrawingContext(Cairo.Surface surface) { _context = new Cairo.Context(surface); - CurrentTransform = Matrix.Identity; } /// @@ -40,15 +40,23 @@ namespace Perspex.Cairo.Media public DrawingContext(Gdk.Drawable drawable) { _context = Gdk.CairoHelper.Create(drawable); - CurrentTransform = Matrix.Identity; } + + private Matrix _transform = Matrix.Identity; /// /// Gets the current transform of the drawing context. /// - public Matrix CurrentTransform + public Matrix Transform { - get; } + get { return _transform; } + set + { + _transform = value; + _context.Matrix = value.ToCairo(); + + } + } /// /// Ends a draw operation. @@ -131,28 +139,30 @@ namespace Perspex.Cairo.Media { var impl = geometry.PlatformImpl as StreamGeometryImpl; - using (var pop = PushTransform(impl.Transform)) + var oldMatrix = Transform; + Transform = impl.Transform * Transform; + + + if (brush != null) { _context.AppendPath(impl.Path); - - if (brush != null) + using (var b = SetBrush(brush, geometry.Bounds.Size)) { - using (var b = SetBrush(brush, geometry.Bounds.Size)) - { - if (pen != null) - _context.FillPreserve(); - else - _context.Fill(); - } + if (pen != null) + _context.FillPreserve(); + else + _context.Fill(); } } + Transform = oldMatrix; if (pen != null) { - using (var p = SetPen(pen, geometry.Bounds.Size)) - { - _context.Stroke(); - } + _context.AppendPath(impl.Path); + using (var p = SetPen(pen, geometry.Bounds.Size)) + { + _context.Stroke(); + } } } @@ -208,31 +218,37 @@ namespace Perspex.Cairo.Media /// /// The clip rectangle. /// A disposable used to undo the clip rectangle. - public IDisposable PushClip(Rect clip) + public void PushClip(Rect clip) { _context.Save(); _context.Rectangle(clip.ToCairo()); _context.Clip(); + } - return Disposable.Create(() => _context.Restore()); + public void PopClip() + { + _context.Restore(); } + readonly Stack _opacityStack = new Stack(); + /// /// Pushes an opacity value. /// /// The opacity. /// A disposable used to undo the opacity. - public IDisposable PushOpacity(double opacity) + public void PushOpacity(double opacity) { - var tmp = opacityOverride; + _opacityStack.Push(opacityOverride); if (opacity < 1.0f) - opacityOverride = opacity; + opacityOverride *= opacity; - return Disposable.Create(() => - { - opacityOverride = tmp; - }); + } + + public void PopOpacity() + { + opacityOverride = _opacityStack.Pop(); } /// diff --git a/src/Gtk/Perspex.Cairo/Media/Imaging/RenderTargetBitmapImpl.cs b/src/Gtk/Perspex.Cairo/Media/Imaging/RenderTargetBitmapImpl.cs index 0b9183d928..4d8f05848a 100644 --- a/src/Gtk/Perspex.Cairo/Media/Imaging/RenderTargetBitmapImpl.cs +++ b/src/Gtk/Perspex.Cairo/Media/Imaging/RenderTargetBitmapImpl.cs @@ -39,7 +39,7 @@ namespace Perspex.Cairo.Media.Imaging Surface.WriteToPng(fileName); } - public IDrawingContext CreateDrawingContext() + public Perspex.Media.DrawingContext CreateDrawingContext() { return _renderTarget.CreateDrawingContext(); } diff --git a/src/Gtk/Perspex.Cairo/Media/TileBrushes.cs b/src/Gtk/Perspex.Cairo/Media/TileBrushes.cs index 94fb19ea0b..98361427f0 100644 --- a/src/Gtk/Perspex.Cairo/Media/TileBrushes.cs +++ b/src/Gtk/Perspex.Cairo/Media/TileBrushes.cs @@ -113,7 +113,7 @@ namespace Perspex.Cairo.Media out drawRect); using (ctx.PushClip(drawRect)) - using (ctx.PushTransform(transform)) + using (ctx.PushPostTransform(transform)) { ctx.Render(visual); } diff --git a/src/Gtk/Perspex.Cairo/RenderTarget.cs b/src/Gtk/Perspex.Cairo/RenderTarget.cs index 333c558e8a..2daab33fbe 100644 --- a/src/Gtk/Perspex.Cairo/RenderTarget.cs +++ b/src/Gtk/Perspex.Cairo/RenderTarget.cs @@ -7,6 +7,7 @@ using Perspex.Cairo.Media; using Perspex.Media; using Perspex.Platform; using Perspex.Rendering; +using DrawingContext = Perspex.Media.DrawingContext; namespace Perspex.Cairo { @@ -50,12 +51,13 @@ namespace Perspex.Cairo /// /// Creates a cairo surface that targets a platform-specific resource. /// - /// A surface wrapped in an . - public IDrawingContext CreateDrawingContext() + /// A surface wrapped in an . + public DrawingContext CreateDrawingContext() { - if(_surface != null) - return new DrawingContext(_surface); - return new DrawingContext(_window.GdkWindow); + var ctx = _surface != null + ? new Media.DrawingContext(_surface) + : new Media.DrawingContext(_window.GdkWindow); + return new DrawingContext(ctx); } public void Dispose() => _surface?.Dispose(); diff --git a/src/Gtk/Perspex.Gtk/GtkPlatform.cs b/src/Gtk/Perspex.Gtk/GtkPlatform.cs index 5b52ac84d1..f6190b3330 100644 --- a/src/Gtk/Perspex.Gtk/GtkPlatform.cs +++ b/src/Gtk/Perspex.Gtk/GtkPlatform.cs @@ -17,6 +17,7 @@ namespace Perspex.Gtk public class GtkPlatform : IPlatformThreadingInterface, IPlatformSettings { private static readonly GtkPlatform s_instance = new GtkPlatform(); + private static Thread _uiThread; public GtkPlatform() { @@ -40,6 +41,7 @@ namespace Perspex.Gtk .Bind().ToConstant(s_instance) .Bind().ToSingleton(); SharedPlatform.Register(); + _uiThread = Thread.CurrentThread; } public bool HasMessages() @@ -79,6 +81,8 @@ namespace Perspex.Gtk Gtk.Application.Invoke(delegate { Signaled?.Invoke(); }); } + public bool CurrentThreadIsLoopThread => Thread.CurrentThread == _uiThread; + public event Action Signaled; } } \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexAttachableXamlMember.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexAttachableXamlMember.cs new file mode 100644 index 0000000000..117f7768f1 --- /dev/null +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexAttachableXamlMember.cs @@ -0,0 +1,33 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Perspex.Markup.Xaml.Binding; +using OmniXaml; +using OmniXaml.Typing; +using System.Reflection; + +namespace Perspex.Markup.Xaml.Context +{ + public class PerspexAttachableXamlMember : AttachableXamlMember + { + public PerspexAttachableXamlMember(string name, + XamlType owner, + MethodInfo getter, + MethodInfo setter, + IXamlTypeRepository xamlTypeRepository, + ITypeFeatureProvider featureProvider) + : base(name, getter, setter, xamlTypeRepository, featureProvider) + { + } + + protected override IXamlMemberValuePlugin LookupXamlMemberValueConnector() + { + return new PerspexXamlMemberValuePlugin(this); + } + + public override string ToString() + { + return "Perspex Attachable XAML Member " + base.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs index fd7a786649..36e1d03ea8 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using System.Reactive.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using Glass; using OmniXaml.ObjectAssembler; using OmniXaml.Typing; @@ -16,9 +17,9 @@ namespace Perspex.Markup.Xaml.Context { public class PerspexXamlMemberValuePlugin : MemberValuePlugin { - private readonly XamlMember _xamlMember; + private readonly MutableXamlMember _xamlMember; - public PerspexXamlMemberValuePlugin(XamlMember xamlMember) + public PerspexXamlMemberValuePlugin(MutableXamlMember xamlMember) : base(xamlMember) { _xamlMember = xamlMember; @@ -73,6 +74,7 @@ namespace Perspex.Markup.Xaml.Context else { var perspexObject = instance as PerspexObject; + var attached = _xamlMember as PerspexAttachableXamlMember; if (perspexObject == null) { @@ -80,13 +82,31 @@ namespace Perspex.Markup.Xaml.Context $"Cannot bind to an object of type '{instance.GetType()}"); } - var property = perspexObject.GetRegisteredProperties() - .FirstOrDefault(x => x.Name == _xamlMember.Name); + PerspexProperty property; + string propertyName; + + if (attached == null) + { + propertyName = _xamlMember.Name; + property = perspexObject.GetRegisteredProperties() + .FirstOrDefault(x => x.Name == propertyName); + } + else + { + // Ensure the OwnerType's static ctor has been run. + RuntimeHelpers.RunClassConstructor(attached.DeclaringType.UnderlyingType.TypeHandle); + + propertyName = attached.DeclaringType.UnderlyingType.Name + '.' + _xamlMember.Name; + + property = perspexObject.GetRegisteredProperties() + .Where(x => x.IsAttached && x.OwnerType == attached.DeclaringType.UnderlyingType) + .FirstOrDefault(x => x.Name == _xamlMember.Name); + } if (property == null) { throw new InvalidOperationException( - $"Cannot find '{_xamlMember.Name}' on '{instance.GetType()}"); + $"Cannot find '{propertyName}' on '{instance.GetType()}"); } var binding = new XamlBinding diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlType.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlType.cs index 0d1fb218b1..dc6b7398f4 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlType.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlType.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reflection; using OmniXaml; using OmniXaml.Typing; using Perspex.Markup.Xaml.Binding; @@ -22,6 +23,14 @@ namespace Perspex.Markup.Xaml.Context return new PerspexXamlMember(name, this, TypeRepository, FeatureProvider); } + protected override AttachableXamlMember LookupAttachableMember(string name) + { + // OmniXAML seems to require a getter and setter even though we don't use them. + var getter = UnderlyingType.GetTypeInfo().GetDeclaredMethod("Get" + name); + var setter = UnderlyingType.GetTypeInfo().GetDeclaredMethod("Set" + name); + return new PerspexAttachableXamlMember(name, this, getter, setter, TypeRepository, FeatureProvider); + } + public override string ToString() { return "Perspex XAML Type " + base.ToString(); diff --git a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj index 987b630907..d34c97c50f 100644 --- a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj +++ b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj @@ -42,6 +42,7 @@ + diff --git a/src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs index da22cafae2..243ef9ef96 100644 --- a/src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reflection; using OmniXaml.Attributes; using Perspex.Controls; using Perspex.Controls.Templates; @@ -20,7 +21,7 @@ namespace Perspex.Markup.Xaml.Templates throw new InvalidOperationException("DataTemplate must have a DataType."); } - return DataType == data.GetType(); + return DataType.GetTypeInfo().IsAssignableFrom(data.GetType().GetTypeInfo()); } public IControl Build(object data) diff --git a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs index 1015bcfa0a..b4efbc324c 100644 --- a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs +++ b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs @@ -81,6 +81,8 @@ namespace Perspex.Markup.Binding { inpc.PropertyChanged -= PropertyChanged; } + + _propertyInfo = null; } private static PropertyInfo FindProperty(object target, string propertyName) diff --git a/src/Perspex.Base/Perspex.Base.csproj b/src/Perspex.Base/Perspex.Base.csproj index abe7582049..b7227d831b 100644 --- a/src/Perspex.Base/Perspex.Base.csproj +++ b/src/Perspex.Base/Perspex.Base.csproj @@ -46,6 +46,7 @@ + diff --git a/src/Perspex.Base/PerspexDisposable.cs b/src/Perspex.Base/PerspexDisposable.cs new file mode 100644 index 0000000000..d3017e4b44 --- /dev/null +++ b/src/Perspex.Base/PerspexDisposable.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Perspex.Platform; + +namespace Perspex +{ + public abstract class PerspexDisposable : IDisposable + { +#if DEBUG_DISPOSE + public string DisposedAt { get; private set; } +#endif + + + public bool IsDisposed { get; private set; } + + public void Dispose() + { + IsDisposed = true; +#if DEBUG_DISPOSE + DisposedAt = PerspexLocator.Current.GetService().GetStackTrace(); +#endif + DoDispose(); + } + + protected void CheckDisposed() + { + if (IsDisposed) + throw new ObjectDisposedException(GetType().FullName +#if DEBUG_DISPOSE + , "Disposed at: \n" + DisposedAt +#endif + + ); + } + + protected abstract void DoDispose(); + } +} diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 696d01893a..27476c881d 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -10,6 +10,7 @@ using System.Reactive.Linq; using System.Reactive.Subjects; using System.Reflection; using Perspex.Reactive; +using Perspex.Threading; using Perspex.Utilities; using Serilog; using Serilog.Core.Enrichers; @@ -300,6 +301,10 @@ namespace Perspex } } + public bool CheckAccess() => Dispatcher.UIThread.CheckAccess(); + + public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess(); + /// /// Clears a 's local value. /// @@ -495,7 +500,7 @@ namespace Perspex BindingPriority priority = BindingPriority.LocalValue) { Contract.Requires(property != null); - + VerifyAccess(); if (property.IsDirect) { property = GetRegistered(property); @@ -556,7 +561,7 @@ namespace Perspex BindingPriority priority = BindingPriority.LocalValue) { Contract.Requires(property != null); - + VerifyAccess(); if (property.IsDirect) { property = (PerspexProperty)GetRegistered(property); @@ -590,7 +595,7 @@ namespace Perspex BindingPriority priority = BindingPriority.LocalValue) { Contract.Requires(property != null); - + VerifyAccess(); if (property.IsDirect) { property = GetRegistered(property); @@ -650,7 +655,7 @@ namespace Perspex BindingPriority priority = BindingPriority.LocalValue) { Contract.Requires(property != null); - + VerifyAccess(); if (property.IsDirect) { property = (PerspexProperty)GetRegistered(property); @@ -687,6 +692,7 @@ namespace Perspex PerspexProperty sourceProperty, BindingPriority priority = BindingPriority.LocalValue) { + VerifyAccess(); _propertyLog.Verbose( "Bound two way {Property} to {Binding} with priority {Priority}", property, @@ -716,6 +722,7 @@ namespace Perspex ISubject source, BindingPriority priority = BindingPriority.LocalValue) { + VerifyAccess(); _propertyLog.Verbose( "Bound two way {Property} to {Binding} with priority {Priority}", property, @@ -733,6 +740,7 @@ namespace Perspex /// The property. public void Revalidate(PerspexProperty property) { + VerifyAccess(); PriorityValue value; if (_values.TryGetValue(property, out value)) @@ -788,6 +796,7 @@ namespace Perspex BindingPriority priority) { Contract.Requires(property != null); + VerifyAccess(); PerspexPropertyChangedEventArgs e = new PerspexPropertyChangedEventArgs( this, @@ -824,6 +833,7 @@ namespace Perspex /// protected bool SetAndRaise(PerspexProperty property, ref T field, T value) { + VerifyAccess(); if (!object.Equals(field, value)) { var old = field; diff --git a/src/Perspex.Base/Platform/IPclPlatformWrapper.cs b/src/Perspex.Base/Platform/IPclPlatformWrapper.cs index 20c836bdaf..68a2a379bf 100644 --- a/src/Perspex.Base/Platform/IPclPlatformWrapper.cs +++ b/src/Perspex.Base/Platform/IPclPlatformWrapper.cs @@ -12,5 +12,6 @@ namespace Perspex.Platform Assembly[] GetLoadedAssemblies(); void PostThreadPoolItem(Action cb); IDisposable StartSystemTimer(TimeSpan interval, Action tick); + string GetStackTrace(); } } diff --git a/src/Perspex.Base/Platform/IPlatformThreadingInterface.cs b/src/Perspex.Base/Platform/IPlatformThreadingInterface.cs index 04cdd5aaba..7acbc30fec 100644 --- a/src/Perspex.Base/Platform/IPlatformThreadingInterface.cs +++ b/src/Perspex.Base/Platform/IPlatformThreadingInterface.cs @@ -26,6 +26,8 @@ namespace Perspex.Platform /// void Signal(); + bool CurrentThreadIsLoopThread { get; } + event Action Signaled; } diff --git a/src/Perspex.Base/Threading/Dispatcher.cs b/src/Perspex.Base/Threading/Dispatcher.cs index ee9555552c..9634eeb4a4 100644 --- a/src/Perspex.Base/Threading/Dispatcher.cs +++ b/src/Perspex.Base/Threading/Dispatcher.cs @@ -26,10 +26,21 @@ namespace Perspex.Threading public Dispatcher(IPlatformThreadingInterface platform) { _platform = platform; + if(_platform == null) + //TODO: Unit test mode, fix that somehow + return; _jobRunner = new JobRunner(platform); _platform.Signaled += _jobRunner.RunJobs; } + public bool CheckAccess() => _platform?.CurrentThreadIsLoopThread() ?? true; + + public void VerifyAccess() + { + if (!CheckAccess()) + throw new InvalidOperationException("Call from invalid thread"); + } + /// /// Runs the dispatcher's main loop. @@ -49,7 +60,7 @@ namespace Perspex.Threading /// public void RunJobs() { - _jobRunner.RunJobs(); + _jobRunner?.RunJobs(); } /// @@ -58,9 +69,9 @@ namespace Perspex.Threading /// The method. /// The priority with which to invoke the method. /// A task that can be used to track the method's execution. - public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeTaskAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal) { - return _jobRunner.InvokeAsync(action, priority); + return _jobRunner?.InvokeAsync(action, priority); } /// @@ -68,9 +79,9 @@ namespace Perspex.Threading /// /// The method. /// The priority with which to invoke the method. - internal void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal) + public void InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal) { - _jobRunner.Post(action, priority); + _jobRunner?.Post(action, priority); } } } \ No newline at end of file diff --git a/src/Perspex.Base/Threading/DispatcherTimer.cs b/src/Perspex.Base/Threading/DispatcherTimer.cs index 688cf57605..159151dc79 100644 --- a/src/Perspex.Base/Threading/DispatcherTimer.cs +++ b/src/Perspex.Base/Threading/DispatcherTimer.cs @@ -177,7 +177,7 @@ namespace Perspex.Threading /// private void InternalTick() { - Dispatcher.UIThread.Post(RaiseTick, _priority); + Dispatcher.UIThread.InvokeAsync(RaiseTick, _priority); } /// diff --git a/src/Perspex.Base/Threading/PerspexSynchronizationContext.cs b/src/Perspex.Base/Threading/PerspexSynchronizationContext.cs index ac7b509740..15941557d4 100644 --- a/src/Perspex.Base/Threading/PerspexSynchronizationContext.cs +++ b/src/Perspex.Base/Threading/PerspexSynchronizationContext.cs @@ -36,14 +36,14 @@ namespace Perspex.Threading /// public override void Post(SendOrPostCallback d, object state) { - Dispatcher.UIThread.Post(() => d(state)); + Dispatcher.UIThread.InvokeAsync(() => d(state)); } /// public override void Send(SendOrPostCallback d, object state) { // TODO: Add check for being on the main thread, we should invoke the method immediately in this case - Dispatcher.UIThread.InvokeAsync(() => d(state)).Wait(); + Dispatcher.UIThread.InvokeTaskAsync(() => d(state)).Wait(); } } } \ No newline at end of file diff --git a/src/Perspex.Base/Threading/SingleThreadDispatcher.cs b/src/Perspex.Base/Threading/SingleThreadDispatcher.cs index 7f1ca48593..5e751833f9 100644 --- a/src/Perspex.Base/Threading/SingleThreadDispatcher.cs +++ b/src/Perspex.Base/Threading/SingleThreadDispatcher.cs @@ -37,6 +37,8 @@ namespace Perspex.Threading () => _timerJobRunner.Post(tick, DispatcherPriority.Normal)); public void Signal() => _evnt.Set(); + //TODO: Actually perform a check + public bool CurrentThreadIsLoopThread => true; public event Action Signaled; } diff --git a/src/Perspex.Controls/Border.cs b/src/Perspex.Controls/Border.cs index 8a70987cb0..bcb1235f25 100644 --- a/src/Perspex.Controls/Border.cs +++ b/src/Perspex.Controls/Border.cs @@ -83,7 +83,7 @@ namespace Perspex.Controls /// Renders the control. /// /// The drawing context. - public override void Render(IDrawingContext context) + public override void Render(DrawingContext context) { var background = Background; var borderBrush = BorderBrush; diff --git a/src/Perspex.Controls/Button.cs b/src/Perspex.Controls/Button.cs index 41736206c9..29eb4c3628 100644 --- a/src/Perspex.Controls/Button.cs +++ b/src/Perspex.Controls/Button.cs @@ -44,6 +44,9 @@ namespace Perspex.Controls public static readonly PerspexProperty CommandProperty = PerspexProperty.Register(nameof(Command)); + /// + /// Defines the property. + /// public static readonly PerspexProperty HotKeyProperty = HotKeyManager.HotKeyProperty.AddOwner