Search code examples
c#wpfuser-controlsdependency-propertiescaliburn.micro

Binding problems between an usercontrol and its parent (another usercontrol) MVVM


I am creating an UserControl with 2 RepeatButtons and one TextBox. So problems begin when i want to Bind some properties....

FYI i am using Caliburn.micro as Framework..

  • i need to have the value of property Interval from the parent to define the time lapse of both repeat button

  • i need to have the MaxValue defined in the parent and use it in the event MouseClick of customcontrol

  • i need to initialize the value of TextBox of custom control from parent, use it in the event mouseclick of custom control to obtain the new value update the TextBox and use the new value changed in the parent...

what i have tested:

CustomRepeatButton.xaml.cs

using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

namespace ChromiumWPF.UserControls
{
    public partial class CustomRepeatButton : UserControl
    {
        public CustomRepeatButton()
        {
            InitializeComponent();
        }

        public string TextValue
        {
            get { return (string)GetValue(TextValueProperty); }
            set { SetValue(TextValueProperty, value); }
        }

        public static DependencyProperty TextValueProperty =
           DependencyProperty.Register("TextValue", typeof(string), typeof(CustomRepeatButton));

        public int IntervalValue
        {
            get { return (int)GetValue(IntervalValueProperty); }
            set { SetValue(IntervalValueProperty, value); }
        }

        public static DependencyProperty IntervalValueProperty =
           DependencyProperty.Register("IntervalValue", 
                                       typeof(int), 
                                       typeof(CustomRepeatButton),
                                       new FrameworkPropertyMetadata(0, 
                                                                     FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)
                                      );

        public int MaxValue
        {
            get { return (int)GetValue(MaxValueProperty); }
            set { SetValue(MaxValueProperty, value); }
        }

        public static DependencyProperty MaxValueProperty =
           DependencyProperty.Register("MaxValue", typeof(int), typeof(CustomRepeatButton));
    :
    :
    //same definition for ResetValue, MinValue and IncrementValue

        private void RepeatButton_Click(object sender, RoutedEventArgs e)
        {
            // click on button + or - and change the value of TextValue i trap in usercontrol Parent
            var RB = sender as RepeatButton;
        }
    }
}

CustomRepeatButton.xaml (see Bindings TextValue and IntervalValue in Style)

<UserControl x:Class="ChromiumWPF.UserControls.CustomRepeatButton"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:ChromiumWPF.UserControls"
             mc:Ignorable="d" 
             DataContext="{Binding RelativeSource={RelativeSource Self}}"
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
        <Style TargetType="TextBox">
            <Style.Setters>
                <Setter Property="FontFamily" Value="Consolas"/>
                <Setter Property="FontWeight" Value="Medium"/>
                <Setter Property="VerticalContentAlignment" Value="Center"/>
                <Setter Property="IsReadOnly" Value="True"/>
                <Setter Property="IsEnabled" Value="True"/>
                <Setter Property="Padding" Value="5"/>
                <Setter Property="Background" Value="Aquamarine"/>
                <Setter Property="Text" Value="{Binding TextValue, Mode=TwoWay}"/>
            </Style.Setters>
        </Style>
        <Style TargetType="RepeatButton">
            <Style.Setters>
                <Setter Property="Interval" Value="{Binding IntervalValue, Mode=TwoWay}"/>
                <Setter Property="Background" Value="Gray"/>
                <EventSetter Event="Click" Handler="RepeatButton_Click"/>
            </Style.Setters>
        </Style>
    </UserControl.Resources>
   <Grid>
        <StackPanel Orientation="Horizontal">
            <RepeatButton Content="-"  />
            <TextBox />
            <RepeatButton Content="+"  />
        </StackPanel>
    </Grid>
</UserControl>

ChromeViewModel.cs

private string adress;
public string Adress
{
    get { return adress; }
    set 
    { 
        adress = value; 
        NotifyOfPropertyChange(() => Adress); 
    }
}
private int _test;
public int Test
{
    get { return _test; }
    set
    {
        _test = value;
        NotifyOfPropertyChange(() => Test);
    }
}
private int _maxV;
public int MaxV
{
    get { return _maxV; }
    set
    {
        _maxV = value;
        NotifyOfPropertyChange(() => MaxV);
    }
}

ChromeView.xaml

<UserControl x:Class="ChromiumWPF.Views.ChromeView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:cal="http://www.caliburnproject.org"
             xmlns:uc="clr-namespace:ChromiumWPF.UserControls"
             xmlns:local="clr-namespace:ChromiumWPF.Views" 
             xmlns:vm="clr-namespace:ChromiumWPF.ViewModels" 
             mc:Ignorable="d" d:DataContext="{d:DesignInstance Type=vm:ChromeViewModel}"
             d:DesignHeight="800" d:DesignWidth="1200">
    <Grid ShowGridLines="True">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="auto"/>
        </Grid.ColumnDefinitions>
        <StackPanel Grid.Column="0" Orientation="Vertical" VerticalAlignment="Top"
                    HorizontalAlignment="Center">
            <ComboBox x:Name="Months" Background="Aqua" Height="30" Width="100"
                      VerticalContentAlignment="Center"/>

            <uc:CustomRepeatButton TextValue="{Binding Adress}"
                                   IncrementValue="10" 
                                   IntervalValue="{Binding Test}" 
                                   MaxValue="{Binding MaxV}" 
                                   MinValue="900" 
                                   ResetValue="1080" />
        </StackPanel>
    </Grid>
</UserControl>

So i have problem with Test, Adress and MaxV (only Bindings in relation with the customControl).

I dont see why the bindings is in error, the definition of DP and properties seems ok for me?? is it a problem of DataContext? Maybe i have to set the DataContext to ChromeViewModel? if yes how i could do that?... Thanks for help..

errors displayed:

System.Windows.Data Error: 40 : BindingExpression path error: 'Adress' property not found on 'object' ''CustomRepeatButton' (Name='')'. BindingExpression:Path=Adress; DataItem='CustomRepeatButton' (Name=''); target element is 'CustomRepeatButton' (Name=''); target property is 'TextValue' (type 'String')
System.Windows.Data Error: 40 : BindingExpression path error: 'Test' property not found on 'object' ''CustomRepeatButton' (Name='')'. BindingExpression:Path=Test; DataItem='CustomRepeatButton' (Name=''); target element is 'CustomRepeatButton' (Name=''); target property is 'IntervalValue' (type 'Int32')
System.Windows.Data Error: 40 : BindingExpression path error: 'MaxV' property not found on 'object' ''CustomRepeatButton' (Name='')'. BindingExpression:Path=MaxV; DataItem='CustomRepeatButton' (Name=''); target element is 'CustomRepeatButton' (Name=''); target property is 'MaxValue' (type 'Int32')

Solution

  • In WPF the dependency property system uses value precedence. For example, the DataContext value of a FrameworkElement is implicitly inherited from the parent. If you set the value explicitly you will override the inherited value (the parent's DataContext will be ignored by the property system).

    In general you want a custom control to inherit the parents DataContext so that external data bindings assigned to the dependency properties.

    The external Binding uses the current DataContext as source (implicitly):

    <UserControl>
    
      <!-- Binding expects the DataContext of CustomRepeatButton 
           to be of type ChromeViewModel -->
      <uc:CustomRepeatButton TextValue="{Binding Adress}" />
    </UserControl>
    

    The following local Binding overrides the inherited DataContext value (taken from your code):

    <UserControl x:Class="ChromiumWPF.UserControls.CustomRepeatButton"
                 DataContext="{Binding RelativeSource={RelativeSource Self}}">
    
    </UserControl>
    

    Because of this Binding, the DataContext of CustomRepeatButton is the CustomRepeatButton control itself. Every Binding that uses the implicit binding syntax ({Binding path}) will now bind to the CustomRepeatButton. This explains the error message that states that the source object is of type CustomRepeatButton : "BindingExpression path error: 'Adress' property not found on 'object' ''CustomRepeatButton'".
    This also exemplifies why you should never set the DataContext of a custom control explicitly - unless you want to surprise the user of your control.

    To fix this don't ever set the DataContext of custom control explicitly. In order to bind the internals to the control's properties you must use the proper binding source i.e explicit binding source.

    If the internals are assigned to the Content property of a ContentControl, for example of a UserControl, you must use Binding.RelativeSource and set the RelativeSource.AncestorType property of the RelativeSource extension:

    <UserControl>
      <RepeatButton Interval="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=IntervalValue}" />
    </UserControl>
    

    Bindings are the same when defined in a Style:

    <Style TargetType="RepeatButton">
      <Setter Property="Interval" 
              Value="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=IntervalValue}"/>
    </Style>
    

    If the internals are defined inside a ControlTemplate, you would have to use the TemplateBinding extension (instead of Binding) or use the Binding extension and set the Binding.RelativeSource property using the RelativeSource extension and set its RelativeSource.Mode property to RelativeSourceMode.TemplatedParent:

    
    <UserControl>
      <UserControl.Template>
        <ControlTemplate TargetType="CustomRepeatButton">
      
          <!-- Recommended using the TemplateBinding extension -->
          <RepeatButton Interval="{TemplateBinding IntervalValue}" />
    
          <!-- Using the Binding together with the RelativeSource extension -->
          <RepeatButton Interval="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IntervalValue}" />
        </ControlTemplate>
      </UserControl.Template>
    </UserControl>