0

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

juicebyjustin
  • 434
  • 7
  • 16
  • 1
    You can invoke your converter by raising the `PropertyChanged` event for the `DeviceObj.Name` source property. – mm8 Jun 26 '18 at 13:03
  • 1
    Are you looking for a MVVM solution, or you do not care about it? – Il Vic Jun 26 '18 at 13:19
  • @mm8 it is being invoked by PropertyChanged to change it to my 'modified' color. I need to change it back after saving. The previous modified state will become the present unmodified state. – juicebyjustin Jun 26 '18 at 16:04
  • @IlVic I would love to change it to MVVM but there is so much code in the .xaml.cs files that is a big under taking for this program. – juicebyjustin Jun 26 '18 at 16:04

2 Answers2

1

You have to paste a bool Saved property to your DeviceObj and handle it, if Name or something else been changed.

ViewModel:

public class Device : INotifyPropertyChanged
{
    public string Name
    {
        get
        {
            return _name;
        }
        set
        {
            if (value != _name)
            {
                _name = value;
                Saved = false;
                NotifyPropertyChanged(nameof(Name));
            }
        }
    }
    private string _name;


    public bool Saved
    {
        get
        {
            return _saved;
        }
        set
        {
            if (value != _saved)
            {
                _saved = value;
                NotifyPropertyChanged(nameof(Saved));
            }
        }
    }
    private bool _saved = true;

    public void Save()
    {
        //Saving..
        Saved = true;
    }

    public event PropertyChangedEventHandler PropertyChanged;
    public void NotifyPropertyChanged(string info)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(info));
    }
}


Converter:

public class BoolToSolColBrushConverter : IValueConverter
{
    private static SolidColorBrush changedBr = new SolidColorBrush(Colors.Red);
    private static SolidColorBrush unchangedBr = new SolidColorBrush(Colors.Green);
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        try
        {
            if ((bool)value)
            {
                return unchangedBr;
            }

        }
        catch (Exception)
        {
        }
        return changedBr;
    }

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


XAML:

<TextBox Text="{Binding Name}" Background="{Binding Saved, Converter={StaticResiurce BoolToSolColBrushConverter}}" />
Rekshino
  • 6,954
  • 2
  • 19
  • 44
  • Thank you for the converter, but I don't see this being the best option. There are about 15-20 objects with 5-15 properties each that I will have to track the save state. This is an elegant solution of which I might end up using if my user base does not appreciate the control 'glitch.' Edit: it is not the object that I need to track the modified state, but each property of the object. – juicebyjustin Jun 26 '18 at 16:06
  • 1
    @JMooney How do you save each single property then? Where do you know from, that editing is complete? – Rekshino Jun 27 '18 at 06:12
  • 1
    One of the possibilities would be to write a behaviour with a pair of dependency properties for the brushes, for you could set them in XAML, which handles *TargetUpdated* and *SourceUpdated* events. – Rekshino Jun 27 '18 at 06:37
  • There are multiple controls that function this way...The control has many objects with multiple properties. The properties of the objects are data bound to XAML. When the user saves, the Save() method is called on the control which submits all changes for each object & their properties to the db. It is within this Save() that I will call UpdateBindings(). Or my last resort, call ResetModifiedState() on each object which resets the modified state for each property (much code). – juicebyjustin Jun 27 '18 at 14:37
  • How would TargetUpdated/SourceUpdated behavior work after having implemented code like II Vic has shown? – juicebyjustin Jun 27 '18 at 14:37
  • 1
    @JMooney Just try it and see how they will work. With behaviors you can avoid bindings for backgrounds at all and set them(backgrounds) directly from behavior. Usually there is a base class for the VM objects, so if you add Saved to this base class, then you will have it in all derived objects. – Rekshino Jun 27 '18 at 14:51
1

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.

Il Vic
  • 5,576
  • 4
  • 26
  • 37
  • Thank you, that is perfect for my text boxes. I did not mention in my original question that there are data grids having MultiBindings too. I updated my question to include the XAML for the data grid column, and the generalized method if used for updating the bindings. – juicebyjustin Jun 27 '18 at 14:29
  • I did not say in my last comment that my DataGridCells background color is not updating with your implementation. I believe it is because MultiBinding is buried in a DataGridTextColum.CellStyle DataTrigger. – juicebyjustin Jun 27 '18 at 14:39
  • I created a project to show what I have and what doesn't work: https://github.com/jmooney5115/clear-multibinding. I hope you get a change to take a look. – juicebyjustin Jun 27 '18 at 15:16
  • 1
    I am sorry @JMooney but I do not know a way for "refreshing" a `MultiBinding` placed inside a `DataTrigger`. attached to a `DataGridCell`. Maybe you should reconsider your strategy – Il Vic Jun 29 '18 at 07:08