Update MultiBinding on DataGridCell

2019-03-04 13:25发布

I am integrating property change detection in an application by way of MultiBinding and IMultiValueConverter. So when a user makes changes to a 'DataGrid', the DataGridCell' changes background color. My issue is that when the user saves their work I cannot remove the changed background without a screen flicker. I can doDataContext = nullthenDataContext = this` but it causes a screen flicker. I cannot call an update binding to reset the MultiBinding to default.

Q: How can I update the MultiBinding on a DataGridCell like below?

Unfortunately this is not MVVM. I created a project that shows the issue: https://github.com/jmooney5115/clear-multibinding

This solution works for TextBox but not DataGridCell:

foreach (TextBox textBox in FindVisualChildren<TextBox>(this))
{
    multiBindingExpression = BindingOperations.GetMultiBindingExpression(textBox, TextBox.BackgroundProperty);
    multiBindingExpression.UpdateTarget();
}

This is the multi binding for a data grid cell The multi value converter takes the original value and the modified value. If the value is changed it returns true to set the background color to LightBlue. If false, the background is the default color.

<DataGrid.Columns>
    <DataGridTextColumn Header="Destination Tag 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>

Here is the multi value converter I'm using:

/// <summary>
/// https://stackoverflow.com/questions/1224144/change-background-color-for-wpf-textbox-in-changed-state
/// 
/// Property changed and display it on a datagrid.
/// 
/// Boolean Converter
/// </summary>

public class BackgroundColorConverterBool : IMultiValueConverter
{
    /// <summary>
    /// 
    /// </summary>
    /// <param name="values"></param>
    /// <param name="targetType"></param>
    /// <param name="parameter"></param>
    /// <param name="culture"></param>
    /// <returns>True is property has changed</returns>
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values[0] is null || values[1] is null) return false;


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

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

Update

Using the solution marked as my answer I was able to expand on it to generalize the UpdateState() method.

foreach (var prop in this.GetType().GetProperties())
    _memo[prop.Name] = prop.GetValue(this);

1条回答
forever°为你锁心
2楼-- · 2019-03-04 13:42

I wouldn't rely on OneTime binding mode and tricks with clearing binding to track changes in data. Implement something like Memento pattern instead.

Add this code which stores state in Item class:

private Dictionary<string, object> _memo = new Dictionary<string, object>();
public object this[string key]
{
    get 
    {
        object o;
        _memo.TryGetValue(key, out o);
        return o;
    }
}

public void UpdateState()
{
    _memo["Name"] = Name;
    _memo["Description"] = Description;
    _memo["Alias"] = Alias;
    _memo["Value"] = Value;
    OnPropertyChanged("Item[]");
}

to make indexer this[] work you have to rename class (e.g. to ItemVm), because class name and member name can't be the same, and .NET uses "Item" as indexer property name.

note that notifications for indexer have "Item[]" format, and VerifyProperty() method should be fixed too:

private void VerifyProperty(string propertyName)
{
    if (propertyName == null || propertyName == "Item[]")
        return;

now, to use unmodified value in window, bind to indexer like this:

<Style TargetType="{x:Type DataGridCell}">
    <Style.Triggers>
        <DataTrigger Value="True">
            <DataTrigger.Binding>
                <MultiBinding Converter="{StaticResource BackgroundColorConverterBool}">
                    <Binding Path="Value"   />
                    <Binding Path="[Value]" />
                </MultiBinding>
            </DataTrigger.Binding>

            <Setter Property="Background" Value="LightBlue"/>
        </DataTrigger>
    </Style.Triggers>
</Style>

save initial state when creating items:

for(var i = 0; i < 100; i++)
{
    Items.Add(new ItemVm
    {
        Alias = string.Format("Item {0}", i.ToString()),
        Description = string.Format("Description {0}", i.ToString()),
        Name = string.Format("Name {0}", i.ToString()),
        Value = string.Format("Value {0}", i.ToString())
    });
    Items[i].UpdateState();
}

and save changes in state on button click:

private void Button_Click(object sender, RoutedEventArgs e)
{
    UIHelper.UpdateDataBindings<Control>(this);

    foreach(var item in Items)
    {
        item.UpdateState();
    }
}
查看更多
登录 后发表回答