From fe6a735c6f375cf985a36cd8135efd88cc991c54 Mon Sep 17 00:00:00 2001 From: Maksym Katsydan Date: Tue, 18 Aug 2020 14:11:41 -0400 Subject: [PATCH 01/59] Create DataGrid Fluent.xaml theme based on Default --- samples/ControlCatalog/App.xaml | 3 +- samples/ControlCatalog/App.xaml.cs | 14 + .../Themes/Fluent.xaml | 294 ++++++++++++++++++ 3 files changed, 309 insertions(+), 2 deletions(-) create mode 100644 src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index bab57f3544..9bac320c79 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -1,8 +1,7 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From cd180770fbd46ff4f33721ed5ca81f477c72c0db Mon Sep 17 00:00:00 2001 From: Kiminuo Date: Wed, 19 Aug 2020 12:33:14 +0200 Subject: [PATCH 02/59] AppBuilderBase: Allow to specify app factory. --- src/Avalonia.Controls/AppBuilderBase.cs | 17 +++++++ .../AppBuilderTests.cs | 44 ++++++++++++++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index d69052ad3d..f616a42cac 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -88,6 +88,23 @@ namespace Avalonia.Controls }; } + /// + /// Begin configuring an . + /// + /// Factory function for . + /// The subclass of to configure. + /// is useful for passing of dependencies to . + /// An instance. + public static TAppBuilder Configure(Func appFactory) + where TApp : Application + { + return new TAppBuilder() + { + ApplicationType = typeof(TApp), + _appFactory = appFactory + }; + } + protected TAppBuilder Self => (TAppBuilder)this; public TAppBuilder AfterSetup(Action callback) diff --git a/tests/Avalonia.Controls.UnitTests/AppBuilderTests.cs b/tests/Avalonia.Controls.UnitTests/AppBuilderTests.cs index b5004bc6a5..06ec9158fe 100644 --- a/tests/Avalonia.Controls.UnitTests/AppBuilderTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AppBuilderTests.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.UnitTests; +using System; +using Avalonia.Controls.UnitTests; using Avalonia.Platform; using Xunit; @@ -18,6 +19,22 @@ namespace Avalonia.Controls.UnitTests { } + public class AppWithDependencies : Application + { + public AppWithDependencies() + { + throw new NotSupportedException(); + } + + public AppWithDependencies(object dependencyA, object dependencyB) + { + DependencyA = dependencyA; + DependencyB = dependencyB; + } + public object DependencyA { get; } + public object DependencyB { get; } + } + public class DefaultModule { public static bool IsLoaded = false; @@ -53,7 +70,30 @@ namespace Avalonia.Controls.UnitTests IsLoaded = true; } } - + + [Fact] + public void UseAppFactory() + { + using (AvaloniaLocator.EnterScope()) + { + ResetModuleLoadStates(); + + Func appFactory = () => new AppWithDependencies(dependencyA: new object(), dependencyB: new object()); + + var builder = AppBuilder.Configure(appFactory) + .UseWindowingSubsystem(() => { }) + .UseRenderingSubsystem(() => { }) + .UseAvaloniaModules() + .SetupWithoutStarting(); + + AppWithDependencies app = (AppWithDependencies)builder.Instance; + Assert.NotNull(app.DependencyA); + Assert.NotNull(app.DependencyB); + + Assert.True(DefaultModule.IsLoaded); + } + } + [Fact] public void LoadsDefaultModule() { From 793f1b7453993255f544a9ae561187896ccc24ac Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 9 Jun 2020 17:44:05 +0200 Subject: [PATCH 03/59] Allow support at CanExecute when binding a method --- .../AlwaysEnabledDelegateCommand.cs | 41 ---- .../Data/Converters/DefaultValueConverter.cs | 2 +- .../Converters/MethodToCommandConverter.cs | 182 ++++++++++++++++++ .../Metadata/DependsOnAttribute.cs | 2 +- 4 files changed, 184 insertions(+), 43 deletions(-) delete mode 100644 src/Avalonia.Base/Data/Converters/AlwaysEnabledDelegateCommand.cs create mode 100644 src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs diff --git a/src/Avalonia.Base/Data/Converters/AlwaysEnabledDelegateCommand.cs b/src/Avalonia.Base/Data/Converters/AlwaysEnabledDelegateCommand.cs deleted file mode 100644 index 7f4c83772d..0000000000 --- a/src/Avalonia.Base/Data/Converters/AlwaysEnabledDelegateCommand.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Globalization; -using System.Reflection; -using System.Windows.Input; -using Avalonia.Utilities; - -namespace Avalonia.Data.Converters -{ - class AlwaysEnabledDelegateCommand : ICommand - { - private readonly Delegate action; - - private ParameterInfo parameterInfo; - - public AlwaysEnabledDelegateCommand(Delegate action) - { - this.action = action; - var parameters = action.Method.GetParameters(); - parameterInfo = parameters.Length == 0 ? null : parameters[0]; - } - -#pragma warning disable 0067 - public event EventHandler CanExecuteChanged; -#pragma warning restore 0067 - - public bool CanExecute(object parameter) => true; - - public void Execute(object parameter) - { - if (parameterInfo == null) - { - action.DynamicInvoke(); - } - else - { - TypeUtilities.TryConvert(parameterInfo.ParameterType, parameter, CultureInfo.CurrentCulture, out object convertedParameter); - action.DynamicInvoke(convertedParameter); - } - } - } -} diff --git a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs index 5e80a15059..83f7e02c16 100644 --- a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs @@ -33,7 +33,7 @@ namespace Avalonia.Data.Converters if (typeof(ICommand).IsAssignableFrom(targetType) && value is Delegate d && d.Method.GetParameters().Length <= 1) { - return new AlwaysEnabledDelegateCommand(d); + return new MethodToCommandConverter(d); } if (TypeUtilities.TryConvert(targetType, value, culture, out object result)) diff --git a/src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs b/src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs new file mode 100644 index 0000000000..c638fe56e6 --- /dev/null +++ b/src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs @@ -0,0 +1,182 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Windows.Input; +using Avalonia.Utilities; + +namespace Avalonia.Data.Converters +{ + class MethodToCommandConverter : ICommand + { + readonly static Func AlwaysEnabled = (_) => true; + readonly static MethodInfo tryConvert = typeof(TypeUtilities) + .GetMethod(nameof(TypeUtilities.TryConvert), BindingFlags.Public | BindingFlags.Static); + readonly static PropertyInfo currentCulture = typeof(CultureInfo) + .GetProperty(nameof(CultureInfo.CurrentCulture), BindingFlags.Public | BindingFlags.Static); + readonly Func canExecute; + readonly Action execute; + + public MethodToCommandConverter(Delegate action) + { + var target = action.Target; + var canExecuteMethodName = "Can" + action.Method.Name; + var parameters = action.Method.GetParameters(); + var parameterInfo = parameters.Length == 0 ? null : parameters[0].ParameterType; + + if (parameterInfo == null) + { + execute = CreateExecute(target, action.Method); + } + else + { + execute = CreateExecute(target, action.Method, parameterInfo); + } + + var canExecuteMethod = action.Method.DeclaringType.GetRuntimeMethods() + .FirstOrDefault(m => m.Name == canExecuteMethodName + && m.GetParameters().Length == 1 + && m.GetParameters()[0].ParameterType == typeof(object)); + if (canExecuteMethod == null) + { + canExecute = AlwaysEnabled; + } + else + { + canExecute = CreateCanExecute(target, canExecuteMethod); + var dependencyProperties = canExecuteMethod + .GetCustomAttributes(typeof(Metadata.DependsOnAttribute), true) + .OfType() + .Select(x => x.Name) + .ToArray(); + if (dependencyProperties.Any() + && target is System.ComponentModel.INotifyPropertyChanged inpc) + { + System.ComponentModel.PropertyChangedEventHandler invalidateCanExecuteHandler = (s, e) => + { + if (string.IsNullOrWhiteSpace(e.PropertyName) + || dependencyProperties.Contains(e.PropertyName)) + { + Threading.Dispatcher.UIThread.Post(() => CanExecuteChanged?.Invoke(this, EventArgs.Empty) + , Threading.DispatcherPriority.Input); + } + }; + inpc.PropertyChanged += invalidateCanExecuteHandler; + } + } + + + } + +#pragma warning disable 0067 + public event EventHandler CanExecuteChanged; +#pragma warning restore 0067 + + public bool CanExecute(object parameter) => canExecute(parameter); + + public void Execute(object parameter) => execute(parameter); + + + static Action CreateExecute(object target + , System.Reflection.MethodInfo method) + { + + var parameter = Expression.Parameter(typeof(object), "parameter"); + + var instance = Expression.Convert + ( + Expression.Constant(target), + method.DeclaringType + ); + + + var call = Expression.Call + ( + instance, + method + ); + + + return Expression + .Lambda>(call, parameter) + .Compile(); + } + + static Action CreateExecute(object target + , System.Reflection.MethodInfo method + , Type parameterType) + { + + var parameter = Expression.Parameter(typeof(object), "parameter"); + + var instance = Expression.Convert + ( + Expression.Constant(target), + method.DeclaringType + ); + + Expression body; + + if (parameterType == typeof(object)) + { + body = Expression.Call(instance, + method, + parameter + ); + } + else + { + var arg0 = Expression.Variable(typeof(object), "argX"); + var convertCall = Expression.Call(tryConvert, + Expression.Constant(parameterType), + parameter, + Expression.Property(null, currentCulture), + arg0 + ); + + var call = Expression.Call(instance, + method, + Expression.Convert(arg0, parameterType) + ); + body = Expression.Block(new[] { arg0 }, + convertCall, + call + ); + + } + Action action = null; + try + { + action = Expression + .Lambda>(body, parameter) + .Compile(); + } + catch (Exception ex) + { + throw ex; + } + return action; + } + + static Func CreateCanExecute(object target + , System.Reflection.MethodInfo method) + { + var parameter = Expression.Parameter(typeof(object), "parameter"); + var instance = Expression.Convert + ( + Expression.Constant(target), + method.DeclaringType + ); + var call = Expression.Call + ( + instance, + method, + parameter + ); + return Expression + .Lambda>(call, parameter) + .Compile(); + } + } +} diff --git a/src/Avalonia.Base/Metadata/DependsOnAttribute.cs b/src/Avalonia.Base/Metadata/DependsOnAttribute.cs index 92c6a58170..caee71ebfd 100644 --- a/src/Avalonia.Base/Metadata/DependsOnAttribute.cs +++ b/src/Avalonia.Base/Metadata/DependsOnAttribute.cs @@ -5,7 +5,7 @@ namespace Avalonia.Metadata /// /// Indicates that the property depends on the value of another property in markup. /// - [AttributeUsage(AttributeTargets.Property)] + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] public class DependsOnAttribute : Attribute { /// From 9f3ed1c8fe1de02fd8e98cb43ed5eb5c2575676a Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 9 Jun 2020 17:44:45 +0200 Subject: [PATCH 04/59] Add testing --- .../Data/BindingTests_Method.cs | 100 +++++++++++++++++- 1 file changed, 96 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs index a7a004bd49..e1a7803821 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs @@ -1,5 +1,4 @@ -using System.Reactive.Subjects; -using System.Windows.Input; +using System.ComponentModel; using Avalonia.Controls; using Avalonia.Input; using Avalonia.UnitTests; @@ -56,7 +55,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data Assert.Equal("Called 5", vm.Value); } } - + [Fact] public void Binding_Method_To_TextBlock_Text_Works() { @@ -79,6 +78,68 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data } } + + [Theory] + [InlineData(null, "Not called")] + [InlineData("A", "Do A")] + public void Binding_Method_With_Parameter_To_Command_CanExecute(object commandParameter, string result) + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + +