locked
Custom control binding to interface RRS feed

  • Question

  • I have a custom control in which I want the user of the control to invoke an action (method) on it.  I do not want to use code-behind and want to do everything through a view model (binding).  I read somewhere where you cannot have an Action or Func<> as a dependency property.  So, my solution was to have a dependency property that is an interface.  The interface defines methods which can be called, which in turn invoke methods on the custom control.

    I really want the custom control to initialize the value of this interface dependency property, and the user of it to bind one way, to source, using the value provided by the control.  I simplified this in an example project I created.

    First, the interface and its implementation:

    public interface ICountUpDown {
    	void Increment();
    	void Decrement();
    }
    
    internal class CountUpDown : ICountUpDown {
    
    	private readonly CounterControl _counterControl;
    
    	public CountUpDown(CounterControl counterControl) {
    		_counterControl = counterControl;
    	}
    
    	public void Increment() {
    		_counterControl.Increment();
    	}
    
    	public void Decrement() {
    		_counterControl.Decrement();
    	}
    }
    

    Code-behind in the custom control (CounterControl):

    public CounterControl() {
    	InitializeComponent();
    	CounterText.Text = "0";
    	CountUpDown = new CountUpDown(this);
    }
    
    public static readonly DependencyProperty CountUpDownProperty = DependencyProperty.Register(
    	"CountUpDown", typeof(ICountUpDown), typeof(CounterControl),
    	new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnCountUpDownChanged));
    
    private static void OnCountUpDownChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) {
    	((CounterControl) o).OnCountUpDownChanged((ICountUpDown) e.NewValue, (ICountUpDown) e.OldValue);
    }
    
    public virtual void OnCountUpDownChanged(ICountUpDown newValue, ICountUpDown oldValue) {
    }
    
    public ICountUpDown CountUpDown {
    	get { return (ICountUpDown) GetValue(CountUpDownProperty); }
    	set {SetValue(CountUpDownProperty, value);}
    }
    
    internal void Increment() {
    	Count = Count + 1;
    }
    
    internal void Decrement() {
    	Count = Count - 1;
    }

    The view model code:

    public ICountUpDown CountUpDown { get { return _countUpDown; } set { if (Equals(value, _countUpDown)) return; _countUpDown = value; OnPropertyChanged(); } } private Command _incrementCommand = null; public ICommand Increment { get { return _incrementCommand ?? (_incrementCommand = new Command((o) => true, ExecuteIncrement)); } } private void ExecuteIncrement(object o) { if (CountUpDown != null) { CountUpDown.Increment(); } } private Command _decrementCommand = null; private ICountUpDown _countUpDown; public ICommand Decrement { get { return _decrementCommand ?? (_decrementCommand = new Command((o) => true, ExecuteDecrement)); } } private void ExecuteDecrement(object o) { if (CountUpDown != null) { CountUpDown.Decrement(); } }

    (Command is a simple implementation of ICommand).

    Finally, the XAML for my main window that uses the CounterControl

    <controlDelegateTest:CounterControl Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" 
    Count="{Binding Count}" 
    CountUpDown="{Binding CountUpDown, Mode=OneWayToSource}"/>
    <Button Width="50" Grid.Row="1" Grid.Column="0" Content="Up" Command="{Binding Increment}" />
    <Button Width="50" Grid.Row="1" Grid.Column="1" Content="Down" Command="{Binding Decrement}" />

    When I run this code, the CountUpDown dependency property is initialize by the constructor of CounterControl, as expected.  However, when the main window's InitializeComponent() is called, the CountUpDown dependency property is set to null.  Apparently the binding is causing the dependency property to be set from the view model's CountUpDown property, which is null.  I use "OneWayToSource", which I assumed would set the property on the view model from the current value in the control.  Is that not the case in OneWayToSource binding?

    Whenever I use a TextBlock, the Text is OneWayToSource.  I've used this in custom controls and it works as expected.  How is my OneWayToSource binding any different than a TextBlock's Text property?

    I don't want the view model to have to create an instance of the ICountUpDown interface.  I want the implementation of this interface to be up to the custom control.  Ideally, I want this dependency property to be read only.  When I tried to make it a readonly dependency property, the compiler failed saying it couldn't do the binding.  I find that curious, since again, I've implement read-only dependency properties that I can bind TextBlock's Text to.

    If there's a simpler way to invoke methods on custom controls, I'm all ears.  I don't want to use routed events or code-behind.

    Sorry for the rather long question, but I don't know how to ask it any simpler and I can find nothing in my searches on this topic.


    Friday, February 24, 2017 4:29 PM

Answers

  • Well, I'll answer my own question...

    When my custom control is created and the constructor called, I set the dependency property value calling SetValue(property, value).  When the control binding is declared in the XAML of the user of the control, the value of the dependency property is whatever the default is declared as.  Since dependency properties and their defaults are static, the only safe value for a reference type would be null, which is what I'm using in this case.  So when the binding is created, the source of the binding gets set to null, which is the default value of the dependency property.

    I found a solution which does what I wanted all along.  In the custom control, I subscribe to the OnLoaded event.  In the event handler, I set the dependency property to a valid value.  This causes the source of the binding (in my view model) to get updated with this value, which is what I wanted all along.

    The fundamental issue is that the assignment to the dependency property was done in the constructor of the control, before the binding occurred in the XAML of the user of the control.  What I failed to recognize is that at the time the binding is created, it uses the default (static) value for the dependency property and sets the source (bound property) to that value, not the value used in the constructor and assignment using SetValue().
    Tuesday, February 28, 2017 7:29 PM

All replies


  • Hi  John,

    >>  Is that not the case in OneWayToSource binding?

    OneWayToSource: it updates the source property when the target property changes. One example scenario is if you only need to re-evaluate the source value from the UI.
    Data Binding Overview:
    https://msdn.microsoft.com/en-us/library/ms752347(v=vs.110).aspx

    >> When I tried to make it a readonly dependency property, the compiler failed saying it couldn't do the binding.

    I try the following code to make CountUpDown to be readonly, it is working.

      public ICountUpDown CountUpDown
            {
                get { return (ICountUpDown)GetValue(CountUpDownProperty); }
                protected set { SetValue(CountUpDownProperty, value); }
            }


    >>I don't want to use routed events or code-behind.

    I suggest you can try to use the routed events in WPF.

    Routed Events Overview:
    https://msdn.microsoft.com/en-us/library/ms742806%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396

    Best Regards,

    Yohann Lu


    MSDN Community Support
    Please remember to click "Mark as Answer" the responses that resolved your issue, and to click "Unmark as Answer" if not. This can be beneficial to other community members reading this thread. If you have any compliments or complaints to MSDN Support, feel free to contact MSDNFSF@microsoft.com.

    Monday, February 27, 2017 6:41 AM
  • You state that "OneWayToSource: it updates the source property when the target property changes."  I set the CountUpDown dependency property in the constructor of the CountControl.  By the time the binding is made, the target (dependency property in CounterControl) is already set.  It's not changing.  Is there any way to set a dependency property (read-only or not) in the constructor of a custom control and have a user bind to it without attempting to change its value?

    If I make the change you suggest where the setter of CountUpDown is protected, I get the compiler error:

    C:\GIT\Sandbox\ControlDelegateTest\ControlDelegateTest\MainWindow.xaml(27,21): error MC3080: The property 'CounterControl.CountUpDown' cannot be set because it does not have an accessible set accessor. Line 27 Position 21.

    The XAML code that fails (3rd line):

    <controlDelegateTest:CounterControl Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" 
      Count="{Binding Count}" 
      CountUpDown="{Binding CountUpDown, Mode=OneWayToSource}"/>

    I am totally confused by this error.  I still do not understand the OneWayToSource seting.  Further, I would like to make the dependency property "CountUpDown" read-only.  I thought the XAML would look at the dependency property (which is still public), not the regular property.

    My question is simply this:

    Is there any way to set a dependency property value in the constructor of a custom control and have the user of that control bind to it in a read-only fashion, using the value set by the constructor of the control?

    Monday, February 27, 2017 2:38 PM
  • Well, I'll answer my own question...

    When my custom control is created and the constructor called, I set the dependency property value calling SetValue(property, value).  When the control binding is declared in the XAML of the user of the control, the value of the dependency property is whatever the default is declared as.  Since dependency properties and their defaults are static, the only safe value for a reference type would be null, which is what I'm using in this case.  So when the binding is created, the source of the binding gets set to null, which is the default value of the dependency property.

    I found a solution which does what I wanted all along.  In the custom control, I subscribe to the OnLoaded event.  In the event handler, I set the dependency property to a valid value.  This causes the source of the binding (in my view model) to get updated with this value, which is what I wanted all along.

    The fundamental issue is that the assignment to the dependency property was done in the constructor of the control, before the binding occurred in the XAML of the user of the control.  What I failed to recognize is that at the time the binding is created, it uses the default (static) value for the dependency property and sets the source (bound property) to that value, not the value used in the constructor and assignment using SetValue().
    Tuesday, February 28, 2017 7:29 PM