WPF DataGrid - 将 TimeSeries 与 MultiBinding 相结合,丢失更改通知。为什么?

标签 wpf data-binding observablecollection

我有一个类,它有两个 ObservableCollection,其中 TimeValue 是带有更改通知的自定义日期时间/值配对(通过 INotifyPropertyChanged)。我将这些称为“目标”和“实际值”。

当我将它们绑定(bind)到图表时,一切都很完美,我得到了两个 LineSeries。如果我将其中一个绑定(bind)到 DataGrid,其中包含“日期”列和“值”列,则可以再次完美运行。我什至得到了我需要的 TwoWay 绑定(bind)。

但是,我需要一个包含“日期”列以及目标和实际值各一列的 DataGrid。问题是我需要列出某个范围内的所有日期,而其中一些日期可能在目标、实际值或两者中没有相应的值。

因此,我决定执行一个 MultiBinding,将目标和实际值作为输入,并输出组合的 TimeSeriesC,只要其中一个原始值没有值,就会输出空值。

它可以工作,但不会响应底层数据的任何更改。

这工作正常(绑定(bind)到一个 ObservableCollection):

<ctrls:DataGrid Grid.Row="1" Height="400" AutoGenerateColumns="False" CanUserDeleteRows="False" SelectionUnit="Cell">
<ctrls:DataGrid.ItemsSource>
    <Binding Path="Targets"/>
    <!--<MultiBinding Converter="{StaticResource TargetActualListConverter}">
        <Binding Path="Targets"/>
        <Binding Path="Actuals"/>
    </MultiBinding>-->
</ctrls:DataGrid.ItemsSource>
<ctrls:DataGrid.Columns>
    <ctrls:DataGridTextColumn Header="Date" Binding="{Binding Date,StringFormat={}{0:ddd, MMM d}}"/>
    <ctrls:DataGridTextColumn Header="Target" Binding="{Binding Value}"/>
    <!--<ctrls:DataGridTextColumn Header="Target" Binding="{Binding Value[0]}"/>
    <ctrls:DataGridTextColumn Header="Actual" Binding="{Binding Value[1]}"/>-->
</ctrls:DataGrid.Columns>

这有效,但仅在首次初始化时有效。对更改通知没有响应:

<ctrls:DataGrid Grid.Row="1" Height="400" AutoGenerateColumns="False" CanUserDeleteRows="False" SelectionUnit="Cell">
<ctrls:DataGrid.ItemsSource>
    <!--<Binding Path="Targets"/>-->
    <MultiBinding Converter="{StaticResource TargetActualListConverter}">
        <Binding Path="Targets"/>
        <Binding Path="Actuals"/>
    </MultiBinding>
</ctrls:DataGrid.ItemsSource>
<ctrls:DataGrid.Columns>
    <ctrls:DataGridTextColumn Header="Date" Binding="{Binding Date,StringFormat={}{0:ddd, MMM d}}"/>
    <!--<ctrls:DataGridTextColumn Header="Target" Binding="{Binding Value}"/>-->
    <ctrls:DataGridTextColumn Header="Target" Binding="{Binding Value[0]}"/>
    <ctrls:DataGridTextColumn Header="Actual" Binding="{Binding Value[1]}"/>
</ctrls:DataGrid.Columns>

这是我的 IMultiValueConverter:

class TargetActualListConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        TimeSeries<double> Targets = values[0] as TimeSeries<double>;
        TimeSeries<double> Actuals = values[1] as TimeSeries<double>;
        DateTime[] range = TimeSeries<double>.GetDateRange(Targets, Actuals);//Get min and max Dates
        int count = (range[1] - range[0]).Days;//total number of days
        DateTime currDate = new DateTime();
        TimeSeries<double?[]> combined = new TimeSeries<double?[]>();
        for (int i = 0; i < count; i++)
        {
            currDate = range[0].AddDays(i);
            double?[] vals = { Targets.Dates.Contains(currDate) ? (double?)Targets.GetValueByDate(currDate) : null, Actuals.Dates.Contains(currDate) ? (double?)Actuals.GetValueByDate(currDate) : null };
            combined.Add(new TimeValue<double?[]>(currDate, vals));
        }
        return combined;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        TimeSeries<double?[]> combined = value as TimeSeries<double?[]>;
        TimeSeries<double> Targets = new TimeSeries<double>();
        TimeSeries<double> Actuals = new TimeSeries<double>();

        foreach (TimeValue<double?[]> tv in combined)
        {
            if(tv.Value[0]!=null)
                Targets.Add(new TimeValue<double>(tv.Date,(double)tv.Value[0]));
            if (tv.Value[1] != null)
                Actuals.Add(new TimeValue<double>(tv.Date, (double)tv.Value[1]));
        }
        TimeSeries<double>[] result = { Targets, Actuals };
        return result;

    }
}

我不能离得太远,因为它显示了值。

我做错了什么? 或者,是否有更简单的方法来做到这一点?

谢谢大家!

最佳答案

看起来这是由转换器引起的。 ObservableCollection 实现 INotifyCollectionChanged,当集合发生更改(添加/删除/替换/移动/重置)时,它会通知 UI。这些都是对集合的更改,而不是集合的内容,因此您之前看到的更新是由于您的类实现了 INotifyPropertyChanged。由于 MultiCoverter 返回新对象的新集合,因此初始集合中的数据不会传播到这些集合,因为没有与原始对象的绑定(bind)可供它们通知。

我建议的第一件事是看一下 CompositeCollection元素并查看是否满足您的需求。

您可以使用以下内容维护原始对象,而不是按原样设置 ItemsSource:

<ctrls:DataGrid.ItemsSource>
    <CompositeCollection>
            <CollectionContainer Collection="{Binding Targets}" />
            <CollectionContainer Collection="{Binding Actuals}" />
       </CompositeCollection>
</ctrls:DataGrid.ItemsSource>

(我假设“不响应基础数据的任何更改”是指更改值,而不是修改集合,如果我不正确,请告诉我,我会更深入地研究它。 )

编辑添加内容
如果这不起作用,替代方法是编写一个新类来包装目标和实际集合。然后可以使用这些包装器创建单个 ObservableCollection。这实际上是比使用 ValueConverter 或使用 CompositeCollection 更好的方法。无论使用哪种方法,您都会失去一些最初存在的功能。通过使用值转换器重新创建集合,它不再直接绑定(bind)到原始对象,因此属性通知可能会丢失。通过使用 CompositeCollection,您不再拥有可以通过添加/删除/移动等进行迭代或修改的单个集合,因为它必须知道要操作哪个集合。

这种类型的包装功能在 WPF 中非常有用,并且是 ViewModel 的非常简化的版本,是 M-V-VM 设计模式的一部分。当您无权访问底层类来添加 INotifyPropertyChanged 或 IDataErrorInfo 时,可以使用它,并且还可以帮助向底层模型添加附加功能,例如状态和交互。

这是一个演示此功能的简短示例,其中我们的两个初始类都具有相同的 Name 属性,并且不实现它们之间不共享的 INotifyPropertyChanged。

public partial class Window1 : Window
{
    public Window1()
    {
        InitializeComponent();

        Foo foo1 = new Foo { ID = 1, Name = "Foo1" };
        Foo foo3 = new Foo { ID = 3, Name = "Foo3" };
        Foo foo5 = new Foo { ID = 5, Name = "Foo5" };
        Bar bar1 = new Bar { ID = 1, Name = "Bar1" };
        Bar bar2 = new Bar { ID = 2, Name = "Bar2" };
        Bar bar4 = new Bar { ID = 4, Name = "Bar4" };

        ObservableCollection<FooBarViewModel> fooBar = new ObservableCollection<FooBarViewModel>();
        fooBar.Add(new FooBarViewModel(foo1, bar1));
        fooBar.Add(new FooBarViewModel(bar2));
        fooBar.Add(new FooBarViewModel(foo3));
        fooBar.Add(new FooBarViewModel(bar4));
        fooBar.Add(new FooBarViewModel(foo5));

        this.DataContext = fooBar;
    }
}

public class Foo
{
    public int ID { get; set; }
    public string Name { get; set; }
}

public class Bar
{
    public int ID { get; set; }
    public string Name { get; set; }
}

public class FooBarViewModel : INotifyPropertyChanged
{
    public Foo WrappedFoo { get; private set; }
    public Bar WrappedBar { get; private set; }

    public int ID
    {
        get
        {
            if (WrappedFoo != null)
            { return WrappedFoo.ID; }
            else if (WrappedBar != null)
            { return WrappedBar.ID; }
            else
            { return -1; }
        }
        set
        {
            if (WrappedFoo != null)
            { WrappedFoo.ID = value; }
            if (WrappedBar != null)
            { WrappedBar.ID = value; }

            this.NotifyPropertyChanged("ID");
        }
    }

    public string BarName
    {
        get
        {
            return WrappedBar.Name;
        }
        set
        {
            WrappedBar.Name = value;
            this.NotifyPropertyChanged("BarName");
        }
    }

    public string FooName
    {
        get
        {
            return WrappedFoo.Name;
        }
        set
        {
            WrappedFoo.Name = value;
            this.NotifyPropertyChanged("FooName");
        }
    }

    public FooBarViewModel(Foo foo)
        : this(foo, null) { }
    public FooBarViewModel(Bar bar)
        : this(null, bar) { }
    public FooBarViewModel(Foo foo, Bar bar)
    {
        WrappedFoo = foo;
        WrappedBar = bar;
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyPropertyChanged(String info)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }
}

然后在窗口中:

 <ListView ItemsSource="{Binding}">
    <ListView.View>
        <GridView>
            <GridViewColumn Header="ID" DisplayMemberBinding="{Binding ID}"/>
            <GridViewColumn Header="Foo Name" DisplayMemberBinding="{Binding FooName}"/>
            <GridViewColumn Header="Bar Name" DisplayMemberBinding="{Binding BarName}"/>
        </GridView>
    </ListView.View>
</ListView>

关于WPF DataGrid - 将 TimeSeries 与 MultiBinding 相结合,丢失更改通知。为什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/875773/

相关文章:

wpf - 发光的 WPF 按钮

wpf - 如何确定 View Model 中定义的属性是否绑定(bind)到 UI 中的任何控件?

c# - 控制不立即使用 INotifyPropertyChanged 更新绑定(bind)属性

silverlight - 如何将 Canvas 绑定(bind)到 Silverlight 中的项目集合

Xamarin Forms ListView 未从 ObservableCollection 更新

c# - WPF 无法识别自定义类库

c# - C# 中的 Moq Application.Current.MainWindow

c# - 将 WPF ComboBox 绑定(bind)到自定义列表

c# - 异步添加到 ObservableCollection(或替代方案)

wpf - 我应该能够使用 ObservableCollection[PSObject] 作为 DataGrid 的 ItemsSource 吗?