0

Let say I have classes like those:

public class ParentModel : INotifyPropertyChanged
{
    // INotifyPropertyChanged pattern implemented ...

    public IChildViewModel CurrentControlModel {
        get { ... } set { /* Notify on changes */ } 
    } 
}

public class ChildModelA : INotifyPropertyChanged, IChildViewModel
{
    // INotifyPropertyChanged pattern implemented ...

    public ICommand Command {
        get { ... } set { /* Notify on changes */ } 
    } 
}

public class ChildModelB : INotifyPropertyChanged, IChildViewModel
{
    // INotifyPropertyChanged pattern implemented ...

    public ICommand Command {
        get { ... } set { /* Notify on changes */ } 
    }  
}

public class ButtonViewModel : INotifyPropertyChanged
{
    ICommand Command get { ... } set { /* Notify on changes */ }    
}

I would like to have Command property to reflect the value of parentModelInstance.CurrentControlModel.Command event if CurrentControlModel changes.

I cannot modify the ButtonViewModel.Command property to be a proxy of the property because it's the view model for all buttons and I don't want to specialize it for every possible button.

If I do

ButtonViewModel viewModel; 
viewModel.Command = parentModelInstance.CurrentControlModel.Command;

it doesn't work because CurrentControlModel can change (it's null at startup for instance). I can listen to PropertyChanged event but it will cumbersome to do that for all properties of the model.

Any easier and cleaner alternative ?

Context

To give a bit of context, it's part of a dynamic toolbar code where you have buttons that can change icon, be disabled or change command, command target etc... depending on what is the current focused control (which can be of different type). CurrentControlModel is the view model of the current focused control.

Fab
  • 14,327
  • 5
  • 49
  • 68
  • Either access the parent VM in xaml via relative source or just bind to `parentModelInstance.CurrentModel.Command` directly, and notify of changes – EpicKip Mar 01 '19 at 14:39
  • 1
    "Any easier and cleaner alternative?". You may want to look into a reactive framework such as [ReactiveUI](https://reactiveui.net/): https://stackoverflow.com/a/22215041/7252182 – mm8 Mar 01 '19 at 14:50
  • @EpicKip I can't do that because ButtonViewModel is used as dynamic viewmodel for DataTemplate with ItemTemplateSelector based on the type of it. – Fab Mar 01 '19 at 15:28
  • Why is there a viewmodel for a button at all? Have you considered using a datatemplateselector to pick a template based on a property instead of type? If you click the button you want to invoke the command in whichever is the currentmodel? Just binding the command to currentmodel.Command ( which is a terrible name btw ) seems kind of obvious. Then make whatever else you're doing work with that. – Andy Mar 01 '19 at 18:32

1 Answers1

0

The journey into the binding land

First solution: One helper to rule them all and with the View Model bind them

It was inspired by ReactiveUI and manual binding on DependencyProperty :

public static BindableProperty<TProperty> Watch<TInstance, TProperty>(
    this TInstance instance,
    Expression<Func<TInstance, TProperty>> expression, 
    BindingMode mode = BindingMode.TwoWay)
{
    return new BindableProperty<TProperty>(instance, 
        GetPath((MemberExpression)expression.Body), mode);
}

public static void BindTo<TInstance, TProperty>(
    this BindableProperty<TProperty> bindable,
    TInstance instance,
    Expression<Func<TInstance, TProperty>> expression) where TInstance 
    : DependencyObject
{
    var getterBody = expression.Body;
    var propertyInfo = (PropertyInfo)((MemberExpression)getterBody).Member;
   var name = propertyInfo.Name;
    var dependencyPropertyName = name + "Property";
    var fieldInfo = typeof(TInstance).GetField(dependencyPropertyName, 
        BindingFlags.Public | BindingFlags.Static);
    var dependencyProperty = (DependencyProperty)fieldInfo.GetValue(null);
    Binding binding = new Binding();
    binding.Source = bindable.Source;
    binding.Path = new PropertyPath(bindable.Path);
    binding.Mode = bindable.Mode;
    binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
    BindingOperations.SetBinding(instance, dependencyProperty, binding);
}

public class BindableProperty<T>
{
    public object Source { get; }
    public string Path { get; }
    public BindingMode Mode { get; }

    public BindableProperty(object source, string path, BindingMode mode)
    {
        Source = source;
        Path = path;
        Mode = mode;
    }
}

ButtonViewModel must derive from DependencyObject and implement the pattern for the Command property

public class ButtonViewModel : DependencyObject
{
    public static readonly DependencyProperty CommandProperty = 
        DependencyProperty.Register("Command", typeof(ICommand), 
        typeof(ButtonViewModel), new PropertyMetadata(default(ICommand)));

    public ICommand Command
    {
        get { return (ICommand) GetValue(CommandProperty); }
        set { SetValue(CommandProperty, value); }
    }
}

Then it can be used like this (for binding paste command to the paste button):

container.Watch(x => x.CurrentControlModel.Commands.Paste)
    .BindTo(pasteButtonViewModel, x => x.Command);

Issues

  • Must setup DependencyProperty pattern for all of properties of view model.
  • Reflexion and expression analysis can raise runtime exceptions.
  • In case a conversion is needed, we must write a proxy doing the conversion and the value modification propagation.

Second solution: Reactive.UI and Fody

Reference the ReactiveUI.WPF and ReactiveUI.Fody, and modify the view model like this

public class ButtonViewModel : ReactiveObject
{
    [Reactive]
    public ICommand Command { get; set; }
}

Then we can bind the two properties like this:

container.WhenAnyValue(x => x.CurrentControlModel.Commands.Paste)
    .BindTo(pasteButtonViewModel, x => x.Command);

Potential issue remaining

  • By not relying on DependencyProperty (apparently), there is a potential issue because we cannot tell the listener that the property is not set (with DependencyProperty.UnsetValue).
  • it's a one way binding.
Fab
  • 14,327
  • 5
  • 49
  • 68