0

I'm quite new with WPF and I might be missing something simple, but I've tried with some simple examples for Custom User Control Binding and they work, but when I try to apply them to our situation, the binding only works when I modify the value in the Custom User Control, but if I change it from other place the Custom User Control does not reflect that change.

We are using Prism and we have a ViewModel and a View to show and edit the properties of a Device. These properties are read from an XML at startup and added to the Device as a Dictionary of Settings. Both Settings and Device implement INotifyPropertyChanged and when a Setting's value changes, the Device raises the property Changed event for that Setting as well.

So when the program is started the values are shown like this, where the red arrow is the new custom user control and blue arrow points the working code directly in the view initial state:

enter image description here

If I change the value in the custom user control, its updated in the other one as well. Binding to source ok:

enter image description here

But if I change it on the other one, its not updated in the custom user control. Binding from source not ok:

enter image description here

Also, if I change the value of the str_pressureUnit condition, the conversion is only performed in the old code. Conversion condition binding not ok:

enter image description here

But the Enable/Disable Control, works correctly for both. Enable ok:

enter image description here

Simple examples with another custom user control and properties from Device that are not Dynamic work ok if I use the `Mode="TwoWay". I was thinking that maybe is a problem with the Multibindings or with the Dynamic Settings configuration, but as the one with EnableProperties work (which is a normal property of the ViewModel) I suspect it might be something related to the Dynamic properties.

This is how our Device class looks like:

  public class Device : DynamicObject, INotifyPropertyChanged
  {
            #region Properties

            ...
            public Dictionary<string, ISetting> DynamicSettings { get; private set; } = new Dictionary<string, ISetting>(); 
            ...
            public Device(SettingDefinitionsProvider settingDefinitionsProvider, ICommunicationChannel communicationChannel, DeviceType deviceType)
            {
                ...
                foreach(ISetting s in DynamicSettings.Values)
                {
                    s.PropertyChanged += OnSettingValueUpdated;
                }

            }
        ...
        #region INotifyPropertyChanged

        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged([CallerMemberName]string propertyname = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyname));
        }

        #endregion //INotifyPropertyChanged

        #region EventHandlers

        private void OnSettingValueUpdated(object sender, PropertyChangedEventArgs e)
        {
            if(sender is ISetting)
            {
                OnPropertyChanged((sender as ISetting).Name);
            }
        }

        #endregion //EventHandlers
  }

And the Setting class throws the PropertyChanged event when its value changes.

The viewModel has a SelectedDevice property and its set as DataContext of the view with the AutoWireViewModel functionality of Prism.

We are also using MultiBinding and some complex converters because some Settings shown value might change depending on other Setting Value, as well the control being enabled, etc. So one entry in the XAML for a Setting might get this big:

<TextBlock Grid.Row="4" Text="{Binding SelectedDevice.SafetyMargin.DisplayName}" Margin="5"/>
    <TextBox Grid.Row="4" Grid.Column="1"  Margin="5">
        <TextBox.IsEnabled>
            <MultiBinding Converter="{conv:EnableControlConverter}">
                <Binding RelativeSource="{RelativeSource AncestorType={x:Type UserControl}, Mode=FindAncestor}" Path="DataContext.SelectedDevice.SafetyMargin"/>
                <Binding RelativeSource="{RelativeSource AncestorType={x:Type UserControl}, Mode=FindAncestor}" Path="DataContext.EnableControls"/>
            </MultiBinding>
        </TextBox.IsEnabled>
        <MultiBinding Converter="{conv:ConversionConverter}">
            <Binding RelativeSource="{RelativeSource AncestorType={x:Type UserControl}, Mode=FindAncestor}" Path="DataContext.SelectedDevice.SafetyMargin"/>
            <Binding RelativeSource="{RelativeSource AncestorType={x:Type UserControl}, Mode=FindAncestor}" Path="DataContext.SelectedDevice.PressureUnit"/>
        </MultiBinding>
    </TextBox>
    <TextBlock Grid.Row="4" Grid.Column="2"  Margin="5">
        <TextBlock.Text>
            <MultiBinding Converter="{conv:UnitLabelConverter}">
                <Binding RelativeSource="{RelativeSource AncestorType={x:Type UserControl}, Mode=FindAncestor}" Path="DataContext.SelectedDevice.SafetyMargin"/>
                <Binding RelativeSource="{RelativeSource AncestorType={x:Type UserControl}, Mode=FindAncestor}" Path="DataContext.SelectedDevice.PressureUnit"/>
            </MultiBinding>
        </TextBlock.Text>
    </TextBlock>

To simplify the addition of new Settings (this is the reason of the reading from xml and so on), I created a new user Control. Its XAML is:

<UserControl ...
         x:Name="parent">

<Grid DataContext="{Binding ElementName=parent}">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>

    <TextBlock Text="{Binding Path=Setting.DisplayName}" Margin="5"/>
    <TextBox Grid.Column="1"  Margin="5">
        <TextBox.IsEnabled>
            <MultiBinding Converter="{conv:EnableControlConverter}">
                <Binding Path="Setting"/>
                <Binding Path="Enabled"/>
            </MultiBinding>
        </TextBox.IsEnabled>
        <MultiBinding Mode="TwoWay" Converter="{conv:ConversionConverter}">
            <Binding Path="Setting"/>
            <Binding Path="ConditionSetting"/>
        </MultiBinding>
    </TextBox>
    <TextBlock Grid.Column="2"  Margin="5">
        <TextBlock.Text>
            <MultiBinding Converter="{conv:UnitLabelConverter}">
                <Binding Path="Setting"/>
                <Binding Path="ConditionSetting"/>
            </MultiBinding>
        </TextBlock.Text>
    </TextBlock>
</Grid>

And the code behind:

public partial class CustomTextBox : UserControl
{
    protected static Logger logger = LogManager.GetCurrentClassLogger();

    public static readonly DependencyProperty SettingProperty =
        DependencyProperty.Register("Setting", typeof(object),
          typeof(CustomTextBox), new PropertyMetadata(""));

    public static readonly DependencyProperty ConditionSettingProperty =
        DependencyProperty.Register("ConditionSetting", typeof(object),
          typeof(CustomTextBox), new PropertyMetadata(""));

    public static readonly DependencyProperty EnabledProperty =
        DependencyProperty.Register("Enabled", typeof(object),
          typeof(CustomTextBox), new PropertyMetadata(""));

    public object Setting
    {
        get { return (object)GetValue(SettingProperty); }
        set { SetValue(SettingProperty, value); }
    }

    public object ConditionSetting
    {
        get { return (object)GetValue(ConditionSettingProperty); }
        set { SetValue(ConditionSettingProperty, value); }
    }

    public object Enabled
    {
        get { return (object)GetValue(EnabledProperty); }
        set { SetValue(EnabledProperty, value); }
    }

    public CustomTextBox()
    {
        InitializeComponent();
    }

}

And this is the new code in the view:

<controls:CustomTextBox Grid.Row="3" 
    Setting="{Binding SelectedDevice.SafetyMargin, Mode=TwoWay}" 
    ConditionSetting="{Binding SelectedDevice.PressureUnit, Mode=TwoWay}" 
    Enabled="{Binding EnableControls}"/>

Any advice will be highly appreciated, thanks in advance.

2 Answers2

0

Did you debug ?

  • Is the callback OnSettingValueUpdated in your Device-Class called in both cases and are there any differences ?

  • Are the setters of ConditionSetting/Setting in your UserControl called ?

I would say that somehow the PropertyChanged is not executed properly or doesn't reach the Property ...

andy meissner
  • 1,202
  • 5
  • 15
  • Hello Andy, thanks for you comment! Yes I've tried debugging, and in both cases the setters for both ConditionSetting/Setting are called (well, in the case of the UserControl, only the value for Setting is changed so only that setter is called), and also the OnSettingValueUpdated seems to trigger the same way in both cases... I'm inclined to think that the event is not reaching the Properties of the UserControl – Alejandro AD Nov 05 '19 at 10:41
  • @AlejandroAD: Try out using 'SetCurrentValue' instead of 'SetValue'. See this question: https://stackoverflow.com/questions/4230698/whats-the-difference-between-dependency-property-setvalue-setcurrentvalue – andy meissner Nov 05 '19 at 12:08
  • The problem probably is that `SetValue()` overrides existing bindings on the property and that's why your changes do not get propagated to the view anymore. The binding is removed as soon as you set the value – andy meissner Nov 05 '19 at 12:18
  • Wow that did sound very promising, but I see no changes on the behaviour after replacing SetValue with SetCurrentValue :(. I've put the str_pressureUnit into its own UserControl now and I've found out that the value is updated in the model, and the textbox that is directly in the view changes its value using the converter, but the background color is not updated so it's not "hearing" the PropertyChanged – Alejandro AD Nov 05 '19 at 12:36
  • Just curious. Did you find a solution ? – andy meissner Nov 07 '19 at 06:54
  • Hello Andy, no not yet unfortunately... I tried using Setting properties skipping all the DynamicObject interface and the same happens. Suspects are the PropertyChanged interface or the DataContext of the UserControl – Alejandro AD Nov 07 '19 at 09:28
  • Just found out that if I add an extra line to my converters with Setting.Value, it works because it hears the PropertyChanged event from Setting directly. – Alejandro AD Nov 07 '19 at 11:13
  • Ok I think I've found the Issue. The problem is that I'm using references to Settings, and when the NotifyPropertyChanged of that Setting is triggered in the view, the DependencyProperty setter must be checking if the Setting has changed, but only the value has changed, not the reference. Therefore, it does not update the value of the binding and the UserControl does not react. I've found the CoerceValueCallback for the DependencyProperties, and I see it triggering every time the NotifyPropertyChanged is launched, but I dont know how to force the DependencyProperty to "update" – Alejandro AD Nov 07 '19 at 12:49
  • I think following solution could work: 1. Raise the property change on your setting `OnPropertyChanged((sender as ISetting));` 2. Add equality members for your ISetting implementations – andy meissner Nov 07 '19 at 13:09
  • You can generate equality members in 2 ways: 1. With Reshaper: Alt + Enter -> Generate -> Equality members – andy meissner Nov 07 '19 at 13:25
  • 2. With VS: https://learn.microsoft.com/en-us/visualstudio/ide/reference/generate-equals-gethashcode-methods?view=vs-2019 – andy meissner Nov 07 '19 at 13:32
  • Thanks again! yes I've been trying that approach, but seems that the problem is that the value of the Setting is already updated when going to compare, and both oldValue and newValue have references to that same Setting so value is also the same. – Alejandro AD Nov 07 '19 at 15:24
0

The problem was using references as values for the DependencyProperties. All the structure is working and the DependencyProperty hears the NotifyPropertyChanged event for the Setting, but it checks the equality before updating the value and then it finds that the reference is the same, not updating it therefore.

Coercion doesn't work because it's done before the check for equality.

The solutions we tried were:

  • Creating a new Setting every time the Setting.Value changes. This works but we were afraid of possible problems of references to old Settings not being updated somewhere in the code that wasn't listening to the PropertyChanged events, for example.

  • Adding additional Bindings to the Converters multibinding in the xaml of the UserControl, especifically the Setting.Value. There is also a PropertyChangedEvent for this property so when it changes, the Converter reevaluates and the UserControl works as we expected. We didn't have to change the actual code besides the XAML so we finally went with this solution.