Search code examples
c#.netdatabasewpfmultibinding

MultiBinding Color Change on Property Change - How to Clear pragmatically?


Edit: I created a sample project displaying what I have done and what doesn't work. https://github.com/jmooney5115/clear-multibinding

I have a WPF application with controls (textbox, datagrid, etc). When a value changes on the control I need to indicate it by changing the background color. After saving changes the background color needs to go back to the unchanged state without reloading the control. This application is not MVVM, don't judge I inherited it.

I have the code working perfectly for changing the color using MultiBinding and a value converter. The problem is I cannot figure out how to reset the background after calling Save() in my code. I have tried doing DataContext = null and then DataContext = this but the control flickers. There has to be a better way.

Q: how can I reset the background to the unchanged state without reloading the control?

MultiBinding XAML - this works by passing a string[] to BackgroundColorConverter. string[0] is the OneTime binding. string1 is the other binding.

<TextBox.Background>
    <MultiBinding Converter="{StaticResource BackgroundColorConverter}">
        <Binding Path="DeviceObj.Name" />
        <Binding Path="DeviceObj.Name" Mode="OneTime" />
    </MultiBinding>
</TextBox.Background>

BackgroundColorConverter.cs

/// <summary>
/// https://stackoverflow.com/questions/1224144/change-background-color-for-wpf-textbox-in-changed-state
/// 
/// Property changed
/// </summary>
public class BackgroundColorConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        var colorRed = (System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#FFB0E0E6");
        var colorWhite = (System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("White");

        var unchanged = new SolidColorBrush(colorWhite);
        var changed = new SolidColorBrush(colorRed);

        if (values.Length == 2)
            if (values[0].Equals(values[1]))
                return unchanged;
            else
                return changed;
        else
            return changed;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Updates

Edit: this is the multi binding for a data grid cell. If the multi binding converter returns true, set the background color to LightBlue. If false, the background is the default color.

<DataGrid.Columns>
    <DataGridTextColumn Header="Name" Binding="{Binding Path=Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" >
        <!-- https://stackoverflow.com/questions/5902351/issue-while-mixing-multibinding-converter-and-trigger-in-style -->
        <DataGridTextColumn.CellStyle>
            <Style TargetType="{x:Type DataGridCell}">
                <Style.Triggers>
                    <DataTrigger Value="True">
                        <DataTrigger.Binding>
                            <MultiBinding Converter="{StaticResource BackgroundColorConverterBool}">
                                <Binding Path="Name"    />
                                <Binding Path="Name" Mode="OneTime" />
                            </MultiBinding>
                        </DataTrigger.Binding>
                    </DataTrigger>

                    <Setter Property="Background" Value="LightBlue"></Setter>
                </Style.Triggers>
            </Style>
        </DataGridTextColumn.CellStyle>
    </DataGridTextColumn>
    .
    .
    .
</DataGrid.Columns>

I made this method to reset the binding of objects after saving.

/// <summary>
/// Update the data binding after a save to clear the blue that could be there when
/// a change is detected.
/// </summary>
/// <typeparam name="T">Type to search for</typeparam>
/// <param name="parentDepObj">Parent object we want to reset the binding for their children.</param>
public static void UpdateDataBinding<T>(DependencyObject parentDepObj) where T : DependencyObject
{
    if (parentDepObj != null)
    {
        MultiBindingExpression multiBindingExpression;

        foreach (var control in UIHelper.FindVisualChildren<T>(parentDepObj))
        {
            multiBindingExpression = BindingOperations.GetMultiBindingExpression(control, Control.BackgroundProperty);
            if (multiBindingExpression != null)
                multiBindingExpression.UpdateTarget();
        }
    }
}

Final Update

This question answers how to use MultiBinding for my purpose on DataGridCell: Update MultiBinding on DataGridCell


Solution

  • IHMO a MVVM solution (as Rekshino proposed) is for sure better than a not-MVVM one. The view model should take care about tracing modified data.

    Anyway since you inherited this application, you have to consider how much time you need for converting the whole code and sometimes it is not possible. So in this case you can force every single multibinding to "refresh" when you save your data.

    Let's suppose this is your XAML (with two or more TextBoxes):

    <StackPanel>
        <TextBox Margin="5" Text="{Binding DeviceObj.Name, Mode=TwoWay}">
            <TextBox.Background>
                <MultiBinding Converter="{StaticResource BackgroundColorConverter}">
                    <Binding Path="DeviceObj.Name" />
                    <Binding Path="DeviceObj.Name" Mode="OneTime" />
                </MultiBinding>
            </TextBox.Background>
        </TextBox>
    
        <TextBox Margin="5" Text="{Binding DeviceObj.Surname, Mode=TwoWay}">
            <TextBox.Background>
                <MultiBinding Converter="{StaticResource BackgroundColorConverter}">
                    <Binding Path="DeviceObj.Surname" />
                    <Binding Path="DeviceObj.Surname" Mode="OneTime" />
                </MultiBinding>
            </TextBox.Background>
        </TextBox>
    
        <Button Content="Save" Click="Button_Click" Margin="5,10,5,10" />
    </StackPanel>
    

    When you click the "Save" Button you can force MultiBindings to update their own targets in this way:

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        MultiBindingExpression multiBindingExpression;
    
        foreach (TextBox textBox in FindVisualChildren<TextBox>(this))
        {
            multiBindingExpression = BindingOperations.GetMultiBindingExpression(textBox, TextBox.BackgroundProperty);
            multiBindingExpression.UpdateTarget();
        }
    }
    

    You can find the FindVisualChildren implementation in this answer. I hope it can help you.