【问题标题】:WPF DataGrid - Combining TimeSeries w/MultiBinding, lose change-notification. Why?WPF DataGrid - 结合 TimeSeries w/MultiBinding,丢失更改通知。为什么?
【发布时间】:2010-10-26 22:07:30
【问题描述】:

我有一个有两个 ObservableCollection 的类,其中 TimeValue 是自定义 DateTime/Value 与更改通知配对(通过 INotifyPropertyChanged)。我称这些为目标和实际值。

当我将这些绑定到图表时,一切正常,我得到了两个 LineSeries。如果我将其中一个绑定到 DataGrid,其中有一列用于“日期”,一列用于“值”,则可以再次完美运行。我什至得到了我需要的双向绑定。

但是,我需要有一个 DataGrid,它有一个“日期”列,以及一个用于 Targets 和 Actuals 的列。问题是我需要列出一个范围内的所有日期,而其中一些日期可能在 Targets、Actuals 或两者中没有对应的值。

所以,我决定做一个 MultiBinding,它以 Targets 和 Actuals 作为输入,并输出一个组合的 TimeSeriesC,只要其中一个原件没有值,就会有空值。

它可以工作,但不会响应基础数据的任何变化。

这很好用(绑定到一个 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;

    }
}

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

我做错了什么? 或者,有没有更简单的方法可以做到这一点?

谢谢大家!

【问题讨论】:

    标签: wpf data-binding observablecollection


    【解决方案1】:

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

    我建议的第一件事是查看CompositeCollection 元素,看看它是否符合您的需求。

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

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

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

    编辑添加
    如果这不起作用,另一种方法是编写一个新类,该类将包装 Target 和 Actual 集合。然后可以使用这些包装器创建单个 ObservableCollection。这实际上是比使用 ValueConverter 或 CompositeCollection 更好的方法。无论哪种方式,您都会失去一些最初存在的功能。通过使用值转换器重新创建集合,它不再直接绑定到原始对象,因此可能会丢失属性通知。通过使用 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>
    

    【讨论】:

    • 谢谢 rmoore。不幸的是,我的设置甚至没有响应 CollectionChanged 事件。我尝试了 w/CompositeCollections,但由于我要显示的集合的大小甚至与我输入的两个集合的大小不同,所以我不知道如何使它工作。奇怪的是,Convert 方法只在初始显示时被调用一次(每个值),并且再也不会被调用。
    • 在这种情况下,在不了解更多信息的情况下解决此问题的最简单方法是创建某种可以合并 Target 和/或 Actual 的包装类。然后,如果您创建这些包装器的单个 ObservableCollection,它应该更容易绑定。
    • 我想我必须这样做。如果我设法按照我的意图进行操作,我会在这里发帖,因为它会更简单。再次感谢 rmoore。无论如何,如果不将其标记为已回答,是否可以奖励您积分?
    • 实际上,这很有效,并没有我想象的那么难。如果您愿意,可以将其写为答案而不是评论,我会将其标记为已回答。谢谢!
    • 我还添加了一个示例,以防其他人偶然发现这一点并且最初没有遵循。 (另外,关于要点:可以在问题、答案和 cmets 上将它们标记为有用或无用,这里有一些关于它的更多信息以及它如何处理积分:stackoverflow.com/questions/130654/…
    猜你喜欢
    • 2011-08-31
    • 1970-01-01
    • 2019-10-01
    • 2020-12-23
    • 1970-01-01
    • 1970-01-01
    • 2018-08-03
    • 2022-12-05
    • 1970-01-01
    相关资源
    最近更新 更多