我有一个有两个ObservableCollection< TimeValue>,其中TimeValue是与change-notification配对的自定义DateTime / Value(通过INotifyPropertyChanged)。我称之为目标和实际值。
当我将这些绑定到图表时,一切都很完美,我得到了两个LineSeries。如果我将其中一个绑定到DataGrid,其中包含“Date”列和“Value”列,则可以再次完美地工作。我甚至得到了我需要的TwoWay绑定。
但是,我需要一个具有“日期”列的DataGrid,以及一个用于Targets和Actuals的列。问题是我需要列出一个范围内的所有日期,而其中一些日期可能没有目标,实际或两者中的相应值。
所以,我决定做一个以Targets和Actuals作为输入的MultiBinding,并输出一个组合的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;
}
}
我不能太远,因为它显示了值。
我做错了什么? 或者,或者,有更简单的方法吗?
全部谢谢!
答案 0 :(得分:2)
看起来这是由转换器引起的。 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>