Browse Source

Merge pull request #4095 from workgroupengineering/features/Binding_Method_CanExecute

[Feature] binding method allow support can execute
pull/4545/head
Jeremy Koritzinsky 6 years ago
committed by GitHub
parent
commit
349d93b7d6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      samples/BindingDemo/MainWindow.xaml
  2. 12
      samples/BindingDemo/ViewModels/MainWindowViewModel.cs
  3. 41
      src/Avalonia.Base/Data/Converters/AlwaysEnabledDelegateCommand.cs
  4. 2
      src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs
  5. 230
      src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs
  6. 2
      src/Avalonia.Base/Metadata/DependsOnAttribute.cs
  7. 1
      src/Avalonia.Base/Properties/AssemblyInfo.cs
  8. 144
      tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs

1
samples/BindingDemo/MainWindow.xaml

@ -116,6 +116,7 @@
<RadioButton Content="Radio Button" IsChecked="{Binding !!BooleanFlag, Mode=OneWay}" Command="{Binding StringValueCommand}" CommandParameter="RadioButton"/>
<TextBox Text="{Binding Path=StringValue}"/>
<Button Content="Nested View Model Button" Name="NestedTest" Command="{Binding NestedModel.Command}" />
<Button Content="Command Method Do" Command="{Binding Do}" x:Name="ToDo"/>
</StackPanel>
</TabItem>
</TabControl>

12
samples/BindingDemo/ViewModels/MainWindowViewModel.cs

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using System.Threading;
using ReactiveUI;
using Avalonia.Controls;
using Avalonia.Metadata;
namespace BindingDemo.ViewModels
{
@ -102,5 +103,16 @@ namespace BindingDemo.ViewModels
get { return _nested; }
private set { this.RaiseAndSetIfChanged(ref _nested, value); }
}
public void Do(object parameter)
{
}
[DependsOn(nameof(BooleanFlag))]
bool CanDo(object parameter)
{
return BooleanFlag;
}
}
}

41
src/Avalonia.Base/Data/Converters/AlwaysEnabledDelegateCommand.cs

@ -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);
}
}
}
}

2
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))

230
src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs

@ -0,0 +1,230 @@
using System;
using System.ComponentModel;
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<object, bool> 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<object, bool> canExecute;
readonly Action<object> execute;
readonly WeakPropertyChangedProxy weakPropertyChanged;
readonly PropertyChangedEventHandler propertyChangedEventHandler;
readonly string[] dependencyProperties;
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);
dependencyProperties = canExecuteMethod
.GetCustomAttributes(typeof(Metadata.DependsOnAttribute), true)
.OfType<Metadata.DependsOnAttribute>()
.Select(x => x.Name)
.ToArray();
if (dependencyProperties.Any()
&& target is INotifyPropertyChanged inpc)
{
propertyChangedEventHandler = OnPropertyChanged;
weakPropertyChanged = new WeakPropertyChangedProxy(inpc, propertyChangedEventHandler);
}
}
}
void OnPropertyChanged(object sender,PropertyChangedEventArgs args)
{
if (string.IsNullOrWhiteSpace(args.PropertyName)
|| dependencyProperties?.Contains(args.PropertyName) == true)
{
Threading.Dispatcher.UIThread.Post(() => CanExecuteChanged?.Invoke(this, EventArgs.Empty)
, Threading.DispatcherPriority.Input);
}
}
#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<object> 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<Action<object>>(call, parameter)
.Compile();
}
static Action<object> 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<object> action = null;
try
{
action = Expression
.Lambda<Action<object>>(body, parameter)
.Compile();
}
catch (Exception ex)
{
throw ex;
}
return action;
}
static Func<object, bool> 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<Func<object, bool>>(call, parameter)
.Compile();
}
internal class WeakPropertyChangedProxy
{
readonly WeakReference<PropertyChangedEventHandler> _listener = new WeakReference<PropertyChangedEventHandler>(null);
readonly PropertyChangedEventHandler _handler;
internal WeakReference<INotifyPropertyChanged> Source { get; } = new WeakReference<INotifyPropertyChanged>(null);
public WeakPropertyChangedProxy()
{
_handler = new PropertyChangedEventHandler(OnPropertyChanged);
}
public WeakPropertyChangedProxy(INotifyPropertyChanged source, PropertyChangedEventHandler listener) : this()
{
SubscribeTo(source, listener);
}
public void SubscribeTo(INotifyPropertyChanged source, PropertyChangedEventHandler listener)
{
source.PropertyChanged += _handler;
Source.SetTarget(source);
_listener.SetTarget(listener);
}
public void Unsubscribe()
{
if (Source.TryGetTarget(out INotifyPropertyChanged source) && source != null)
source.PropertyChanged -= _handler;
Source.SetTarget(null);
_listener.SetTarget(null);
}
void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (_listener.TryGetTarget(out var handler) && handler != null)
handler(sender, e);
else
Unsubscribe();
}
}
}
}

2
src/Avalonia.Base/Metadata/DependsOnAttribute.cs

@ -5,7 +5,7 @@ namespace Avalonia.Metadata
/// <summary>
/// Indicates that the property depends on the value of another property in markup.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class DependsOnAttribute : Attribute
{
/// <summary>

1
src/Avalonia.Base/Properties/AssemblyInfo.cs

@ -9,3 +9,4 @@ using Avalonia.Metadata;
[assembly: InternalsVisibleTo("Avalonia.UnitTests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid")]
[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests")]

144
tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs

@ -1,5 +1,5 @@
using System.Reactive.Subjects;
using System.Windows.Input;
using System;
using System.ComponentModel;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.UnitTests;
@ -56,7 +56,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
Assert.Equal("Called 5", vm.Value);
}
}
[Fact]
public void Binding_Method_To_TextBlock_Text_Works()
{
@ -79,6 +79,111 @@ 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 = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Button Name='button' Command='{Binding Do}' CommandParameter='{Binding Parameter, Mode=OneTime}'/>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = window.FindControl<Button>("button");
var vm = new ViewModel()
{
Parameter = commandParameter
};
button.DataContext = vm;
window.ApplyTemplate();
Assert.NotNull(button.Command);
PerformClick(button);
Assert.Equal(vm.Value, result);
}
}
[Fact]
public void Binding_Method_With_Parameter_To_Command_CanExecute_DependsOn()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Button Name='button' Command='{Binding Do}' CommandParameter='{Binding Parameter, Mode=OneWay}'/>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = window.FindControl<Button>("button");
var vm = new ViewModel()
{
Parameter = null,
};
button.DataContext = vm;
window.ApplyTemplate();
Assert.NotNull(button.Command);
Assert.Equal(button.IsEffectivelyEnabled, false);
vm.Parameter = true;
Threading.Dispatcher.UIThread.RunJobs();
Assert.Equal(button.IsEffectivelyEnabled, true);
}
}
[Fact]
public void Binding_Method_To_Command_Collected()
{
WeakReference<ViewModel> MakeRef()
{
var weakVm = new WeakReference<ViewModel>(null);
{
var vm = new ViewModel()
{
Parameter = null,
};
weakVm.SetTarget(vm);
var canExecuteCount = 0;
var action = new Action<object>(vm.Do);
var command = new Avalonia.Data.Converters.MethodToCommandConverter(action);
command.CanExecuteChanged += (s, e) => canExecuteCount++;
vm.Parameter = 0;
Threading.Dispatcher.UIThread.RunJobs();
vm.Parameter = null;
Threading.Dispatcher.UIThread.RunJobs();
Assert.Equal(2, canExecuteCount);
}
return weakVm;
}
bool IsAlive(WeakReference<ViewModel> @ref)
{
return @ref.TryGetTarget(out var instance)
&& instance is null == false;
}
var vmref = MakeRef();
var beforeCollect = IsAlive(vmref);
GC.Collect();
GC.WaitForPendingFinalizers();
var afterCollect = IsAlive(vmref);
Assert.True(beforeCollect, "Invalid ViewModel instance, it is already collected.");
Assert.False(afterCollect, "ViewModel instance was not collected");
}
static void PerformClick(Button button)
{
button.RaiseEvent(new KeyEventArgs
@ -88,12 +193,43 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
});
}
private class ViewModel
private class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public string Method() => Value = "Called";
public string Method1(int i) => Value = $"Called {i}";
public string Method2(int i, int j) => Value = $"Called {i},{j}";
public string Value { get; private set; } = "Not called";
object _parameter;
public object Parameter
{
get
{
return _parameter;
}
set
{
if (_parameter == value)
{
return;
}
_parameter = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Parameter)));
}
}
public void Do(object parameter)
{
Value = $"Do {parameter}";
}
[Metadata.DependsOn(nameof(Parameter))]
public bool CanDo(object parameter)
{
return ReferenceEquals(null, parameter) == false;
}
}
}
}

Loading…
Cancel
Save