diff --git a/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs b/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs
index 2ab6c26e68..df80931367 100644
--- a/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs
+++ b/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs
@@ -21,7 +21,7 @@ namespace BindingDemo.ViewModels
}
else
{
- throw new ArgumentOutOfRangeException("Value must be less than 10.");
+ throw new ArgumentOutOfRangeException(nameof(value), "Value must be less than 10.");
}
}
}
diff --git a/samples/ControlCatalog/Pages/ContextMenuPage.xaml b/samples/ControlCatalog/Pages/ContextMenuPage.xaml
index 932f3292ff..8f147638b5 100644
--- a/samples/ControlCatalog/Pages/ContextMenuPage.xaml
+++ b/samples/ControlCatalog/Pages/ContextMenuPage.xaml
@@ -10,7 +10,8 @@
HorizontalAlignment="Center"
Spacing="16">
+ Margin="16"
+ Padding="48,48,48,48">
@@ -31,7 +32,24 @@
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs b/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs
index dc73bef07a..96e8b49f89 100644
--- a/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs
@@ -1,5 +1,6 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
+using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
@@ -8,6 +9,7 @@ namespace ControlCatalog.Pages
public ContextMenuPage()
{
this.InitializeComponent();
+ DataContext = new ContextMenuPageViewModel();
}
private void InitializeComponent()
diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml.cs b/samples/ControlCatalog/Pages/MenuPage.xaml.cs
index 5a07cae89f..0a77607719 100644
--- a/samples/ControlCatalog/Pages/MenuPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/MenuPage.xaml.cs
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
+using ControlCatalog.ViewModels;
using ReactiveUI;
namespace ControlCatalog.Pages
@@ -13,51 +14,7 @@ namespace ControlCatalog.Pages
public MenuPage()
{
this.InitializeComponent();
- var vm = new MenuPageViewModel();
-
- vm.MenuItems = new[]
- {
- new MenuItemViewModel
- {
- Header = "_File",
- Items = new[]
- {
- new MenuItemViewModel { Header = "_Open...", Command = vm.OpenCommand },
- new MenuItemViewModel { Header = "Save", Command = vm.SaveCommand },
- new MenuItemViewModel { Header = "-" },
- new MenuItemViewModel
- {
- Header = "Recent",
- Items = new[]
- {
- new MenuItemViewModel
- {
- Header = "File1.txt",
- Command = vm.OpenRecentCommand,
- CommandParameter = @"c:\foo\File1.txt"
- },
- new MenuItemViewModel
- {
- Header = "File2.txt",
- Command = vm.OpenRecentCommand,
- CommandParameter = @"c:\foo\File2.txt"
- },
- }
- },
- }
- },
- new MenuItemViewModel
- {
- Header = "_Edit",
- Items = new[]
- {
- new MenuItemViewModel { Header = "_Copy" },
- new MenuItemViewModel { Header = "_Paste" },
- }
- }
- };
-
- DataContext = vm;
+ DataContext = new MenuPageViewModel();
}
private void InitializeComponent()
@@ -65,51 +22,4 @@ namespace ControlCatalog.Pages
AvaloniaXamlLoader.Load(this);
}
}
-
- public class MenuPageViewModel
- {
- public MenuPageViewModel()
- {
- OpenCommand = ReactiveCommand.CreateFromTask(Open);
- SaveCommand = ReactiveCommand.Create(Save);
- OpenRecentCommand = ReactiveCommand.Create(OpenRecent);
- }
-
- public IReadOnlyList MenuItems { get; set; }
- public ReactiveCommand OpenCommand { get; }
- public ReactiveCommand SaveCommand { get; }
- public ReactiveCommand OpenRecentCommand { get; }
-
- public async Task Open()
- {
- var dialog = new OpenFileDialog();
- var result = await dialog.ShowAsync(App.Current.MainWindow);
-
- if (result != null)
- {
- foreach (var path in result)
- {
- System.Diagnostics.Debug.WriteLine($"Opened: {path}");
- }
- }
- }
-
- public void Save()
- {
- System.Diagnostics.Debug.WriteLine("Save");
- }
-
- public void OpenRecent(string path)
- {
- System.Diagnostics.Debug.WriteLine($"Open recent: {path}");
- }
- }
-
- public class MenuItemViewModel
- {
- public string Header { get; set; }
- public ICommand Command { get; set; }
- public object CommandParameter { get; set; }
- public IList Items { get; set; }
- }
}
diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml
index 625b344b8c..fea55bcb07 100644
--- a/samples/ControlCatalog/SideBar.xaml
+++ b/samples/ControlCatalog/SideBar.xaml
@@ -1,6 +1,14 @@
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/src/Avalonia.Animation/Cue.cs b/src/Avalonia.Animation/Cue.cs
index 52d1609cf9..7da7a9382b 100644
--- a/src/Avalonia.Animation/Cue.cs
+++ b/src/Avalonia.Animation/Cue.cs
@@ -30,7 +30,7 @@ namespace Avalonia.Animation
///
/// Parses a string to a object.
///
- public static object Parse(string value, CultureInfo culture)
+ public static Cue Parse(string value, CultureInfo culture)
{
string v = value;
@@ -70,7 +70,7 @@ namespace Avalonia.Animation
}
}
- public class CueTypeConverter : TypeConverter
+ public class CueTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs
index 2a19f40ecb..7601b64ce9 100644
--- a/src/Avalonia.Base/AvaloniaObject.cs
+++ b/src/Avalonia.Base/AvaloniaObject.cs
@@ -26,6 +26,7 @@ namespace Avalonia
private List _directBindings;
private PropertyChangedEventHandler _inpcChanged;
private EventHandler _propertyChanged;
+ private EventHandler _inheritablePropertyChanged;
private ValueStore _values;
private ValueStore Values => _values ?? (_values = new ValueStore(this));
@@ -56,6 +57,15 @@ namespace Avalonia
remove { _inpcChanged -= value; }
}
+ ///
+ /// Raised when an inheritable value changes on this object.
+ ///
+ event EventHandler IAvaloniaObject.InheritablePropertyChanged
+ {
+ add { _inheritablePropertyChanged += value; }
+ remove { _inheritablePropertyChanged -= value; }
+ }
+
///
/// Gets or sets the parent object that inherited values
/// are inherited from.
@@ -76,8 +86,9 @@ namespace Avalonia
{
if (_inheritanceParent != null)
{
- _inheritanceParent.PropertyChanged -= ParentPropertyChanged;
+ _inheritanceParent.InheritablePropertyChanged -= ParentPropertyChanged;
}
+
var properties = AvaloniaPropertyRegistry.Instance.GetRegistered(this)
.Concat(AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(this.GetType()));
var inherited = (from property in properties
@@ -102,7 +113,7 @@ namespace Avalonia
if (_inheritanceParent != null)
{
- _inheritanceParent.PropertyChanged += ParentPropertyChanged;
+ _inheritanceParent.InheritablePropertyChanged += ParentPropertyChanged;
}
}
}
@@ -379,6 +390,7 @@ namespace Avalonia
internal void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
{
+ LogIfError(property, notification);
UpdateDataValidation(property, notification);
}
@@ -410,6 +422,23 @@ namespace Avalonia
});
}
+ ///
+ /// Logs a binding error for a property.
+ ///
+ /// The property that the error occurred on.
+ /// The binding error.
+ protected internal virtual void LogBindingError(AvaloniaProperty property, Exception e)
+ {
+ Logger.Log(
+ LogEventLevel.Warning,
+ LogArea.Binding,
+ this,
+ "Error in binding to {Target}.{Property}: {Message}",
+ this,
+ property,
+ e.Message);
+ }
+
///
/// Called to update the validation state for properties for which data validation is
/// enabled.
@@ -467,6 +496,11 @@ namespace Avalonia
PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name);
_inpcChanged(this, e2);
}
+
+ if (property.Inherits)
+ {
+ _inheritablePropertyChanged?.Invoke(this, e);
+ }
}
finally
{
@@ -606,7 +640,7 @@ namespace Avalonia
if (notification != null)
{
- notification.LogIfError(this, property);
+ LogIfError(property, notification);
value = notification.Value;
}
@@ -738,6 +772,29 @@ namespace Avalonia
return description?.Description ?? o.ToString();
}
+ ///
+ /// Logs a mesage if the notification represents a binding error.
+ ///
+ /// The property being bound.
+ /// The binding notification.
+ private void LogIfError(AvaloniaProperty property, BindingNotification notification)
+ {
+ if (notification.ErrorType == BindingErrorType.Error)
+ {
+ if (notification.Error is AggregateException aggregate)
+ {
+ foreach (var inner in aggregate.InnerExceptions)
+ {
+ LogBindingError(property, inner);
+ }
+ }
+ else
+ {
+ LogBindingError(property, notification.Error);
+ }
+ }
+ }
+
///
/// Logs a property set message.
///
diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
index 135935498c..e48c671a13 100644
--- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
+++ b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
@@ -21,7 +21,7 @@ namespace Avalonia.Data.Core.Plugins
{
if (method.GetParameters().Length + (method.ReturnType == typeof(void) ? 0 : 1) > 8)
{
- var exception = new ArgumentException("Cannot create a binding accessor for a method with more than 8 parameters or more than 7 parameters if it has a non-void return type.", nameof(method));
+ var exception = new ArgumentException("Cannot create a binding accessor for a method with more than 8 parameters or more than 7 parameters if it has a non-void return type.", nameof(methodName));
return new PropertyError(new BindingNotification(exception, BindingErrorType.Error));
}
diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs
index c11f8ada7e..5a3829167a 100644
--- a/src/Avalonia.Base/IAvaloniaObject.cs
+++ b/src/Avalonia.Base/IAvaloniaObject.cs
@@ -16,6 +16,11 @@ namespace Avalonia
///
event EventHandler PropertyChanged;
+ ///
+ /// Raised when an inheritable value changes on this object.
+ ///
+ event EventHandler InheritablePropertyChanged;
+
///
/// Gets a value.
///
@@ -97,4 +102,4 @@ namespace Avalonia
IObservable source,
BindingPriority priority = BindingPriority.LocalValue);
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Base/IPriorityValueOwner.cs b/src/Avalonia.Base/IPriorityValueOwner.cs
index 8cbf212381..540b1bf19b 100644
--- a/src/Avalonia.Base/IPriorityValueOwner.cs
+++ b/src/Avalonia.Base/IPriorityValueOwner.cs
@@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using System;
using Avalonia.Data;
using Avalonia.Utilities;
@@ -28,6 +29,13 @@ namespace Avalonia
/// The notification.
void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification);
+ ///
+ /// Logs a binding error.
+ ///
+ /// The property the error occurred on.
+ /// The binding error.
+ void LogError(AvaloniaProperty property, Exception e);
+
///
/// Ensures that the current thread is the UI thread.
///
diff --git a/src/Avalonia.Base/Logging/LoggerExtensions.cs b/src/Avalonia.Base/Logging/LoggerExtensions.cs
deleted file mode 100644
index 24e44bf9de..0000000000
--- a/src/Avalonia.Base/Logging/LoggerExtensions.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System;
-using Avalonia.Data;
-
-namespace Avalonia.Logging
-{
- internal static class LoggerExtensions
- {
- public static void LogIfError(
- this BindingNotification notification,
- object source,
- AvaloniaProperty property)
- {
- if (notification.ErrorType == BindingErrorType.Error)
- {
- if (notification.Error is AggregateException aggregate)
- {
- foreach (var inner in aggregate.InnerExceptions)
- {
- LogError(source, property, inner);
- }
- }
- else
- {
- LogError(source, property, notification.Error);
- }
- }
- }
-
- private static void LogError(object source, AvaloniaProperty property, Exception e)
- {
- var level = LogEventLevel.Warning;
-
- if (e is BindingChainException b &&
- !string.IsNullOrEmpty(b.Expression) &&
- string.IsNullOrEmpty(b.ExpressionErrorPoint))
- {
- // The error occurred at the root of the binding chain: it's possible that the
- // DataContext isn't set up yet, so log at Information level instead of Warning
- // to prevent spewing hundreds of errors.
- level = LogEventLevel.Information;
- }
-
- Logger.Log(
- level,
- LogArea.Binding,
- source,
- "Error in binding to {Target}.{Property}: {Message}",
- source,
- property,
- e.Message);
- }
- }
-}
diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs
index c8b434c6f9..89a893577f 100644
--- a/src/Avalonia.Base/PriorityValue.cs
+++ b/src/Avalonia.Base/PriorityValue.cs
@@ -197,7 +197,7 @@ namespace Avalonia
/// The binding error.
public void LevelError(PriorityLevel level, BindingNotification error)
{
- error.LogIfError(Owner, Property);
+ Owner.LogError(Property, error.Error);
}
///
diff --git a/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs
new file mode 100644
index 0000000000..0ade1af249
--- /dev/null
+++ b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs
@@ -0,0 +1,219 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// 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.Reflection;
+using System.Runtime.CompilerServices;
+
+namespace Avalonia.Utilities
+{
+ ///
+ /// Manages subscriptions to events using weak listeners.
+ ///
+ public static class WeakEventHandlerManager
+ {
+ ///
+ /// Subscribes to an event on an object using a weak subscription.
+ ///
+ /// The type of the target.
+ /// The type of the event arguments.
+ /// The event source.
+ /// The name of the event.
+ /// The subscriber.
+ public static void Subscribe(TTarget target, string eventName, EventHandler subscriber)
+ where TEventArgs : EventArgs where TSubscriber : class
+ {
+ var dic = SubscriptionTypeStorage.Subscribers.GetOrCreateValue(target);
+ Subscription sub;
+
+ if (!dic.TryGetValue(eventName, out sub))
+ {
+ dic[eventName] = sub = new Subscription(dic, typeof(TTarget), target, eventName);
+ }
+
+ sub.Add(subscriber);
+ }
+
+ ///
+ /// Unsubscribes from an event.
+ ///
+ /// The type of the event arguments.
+ /// The event source.
+ /// The name of the event.
+ /// The subscriber.
+ public static void Unsubscribe(object target, string eventName, EventHandler subscriber)
+ where TEventArgs : EventArgs where TSubscriber : class
+ {
+ SubscriptionDic dic;
+
+ if (SubscriptionTypeStorage.Subscribers.TryGetValue(target, out dic))
+ {
+ Subscription sub;
+
+ if (dic.TryGetValue(eventName, out sub))
+ {
+ sub.Remove(subscriber);
+ }
+ }
+ }
+
+ private static class SubscriptionTypeStorage
+ where TArgs : EventArgs where TSubscriber : class
+ {
+ public static readonly ConditionalWeakTable