在没有 INotifyPropertyChanged 的情况下更新 ObservableCollection 成员 属性 的绑定
Updating binding for ObservableCollection member property without INotifyPropertyChanged
我有一个 WPF View
绑定到实现 INotifyPropertyChanged
的 ViewModel
。在 View
中,我有一个 DataGrid
,其项目绑定到 ViewModel
中的 ObservableCollection
。 ObservableCollection
成员自己没有实现 INotifyPropertyChanged
。在 DataGrid
中,我有三列 - 两个可编辑的 DatePicker
列和一个只读的 Checkbox
列,当用户选择包含当前日期的日期范围时,应该选中这些列。
绑定在首次加载视图时起作用,但当我更改其他两列中的日期时,Checkbox
的绑定未更新,大概是因为 class MyDomainObject
没有实施 INotifyPropertyChanged
。我真的不想在这里实现那个接口,因为 ObservableCollection
是从网络服务返回的类型,这样做会迫使我创建和维护该类型的副本,而且我有很多我的应用程序中的类似场景。有什么办法可以强制复选框更新吗?如果可能的话,我非常愿意避免使用代码隐藏——如果我必须打破那个规则,我知道该怎么做。
下面的代码应该可以大致说明问题:
XAML:
<DataGrid ItemsSource="{Binding MyDomainObjectCollection}">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Start Date">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding StartDate, StringFormat=d}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<DatePicker SelectedDate="{Binding StartDate, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="End Date">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding EndDate, StringFormat=d}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<DatePicker SelectedDate="{Binding EndDate, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Is Active">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsActive, Mode=OneWay}" IsEnabled="False" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
查看模型:
public class MyViewModel : INotifyPropertyChanged {
ObservableCollection<MyDomainObject> _myDomainObjectCollection;
public ObservableCollection<MyDomainObject> MyDomainObjectCollection {
get { return this._myDomainObjectCollection; }
set { this._myDomainObjectCollection = value; this.OnPropertyChanged(); }
}
[...]
}
域对象:
public class MyDomainObject {
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public bool IsActive {
get { return StartDate < DateTime.Now && EndDate > DateTime.Now; }
}
}
如果您不想在您的域对象上实现 INotifyPropertyChanged
接口,您可以使用与您的 DomainObject 具有相同接口并实现 INotifyPropertyChanged
的 ViewModel 包装您的 DomainObject
public class MyDomainObjectViewModel : INotifyPropertyChanged {
private MyDomainObject _domainObject;
public MyDomainObjectViewModel(MyDomainObject domainObject){
_domainObject = domainObject;
}
public DateTime StartDate {
get{
return _domainObject.StartDate;
}
set{
_domainObject.StartDate = value;
RaisePropertyChanged("StartDate");
RaisePropertyChanged("IsActive");
}
}
public DateTime EndDate {
get{
return _domainObject.EndDate ;
}
set{
_domainObject.EndDate = value;
RaisePropertyChanged("EndDate");
RaisePropertyChanged("IsActive");
}
}
public bool IsActive {
get { return StartDate < DateTime.Now && EndDate > DateTime.Now; }
}
}
您的 ObservableCollection 将是 ObservableCollection<MyDomainObjectViewModel>
.
类型
几年前我遇到过同样的问题。我找到的解决方案使用动态对象作为实现 INotifyPropertyChanged 的代理对象。它通过反射挂钩 getter 和 setter,并在代理更改时更新源对象。
public class DynamicProxy : DynamicObject, INotifyPropertyChanged
{
private readonly PropertyDescriptorCollection mPropertyDescriptors;
protected PropertyInfo GetPropertyInfo(string propertyName)
{
return
ProxiedObject.GetType()
.GetProperties()
.FirstOrDefault(propertyInfo => propertyInfo.Name == propertyName);
}
protected virtual void SetMember(string propertyName, object value)
{
var propertyInfo = GetPropertyInfo(propertyName);
if (propertyInfo.PropertyType == value.GetType())
{
propertyInfo.SetValue(ProxiedObject, value, null);
}
else
{
var underlyingType = Nullable.GetUnderlyingType(propertyInfo.PropertyType);
if (underlyingType != null)
{
var propertyDescriptor = mPropertyDescriptors.Find(propertyName, false);
var converter = propertyDescriptor.Converter;
if (converter != null && converter.CanConvertFrom(typeof (string)))
{
var convertedValue = converter.ConvertFrom(value);
propertyInfo.SetValue(ProxiedObject, convertedValue, null);
}
}
}
RaisePropertyChanged(propertyName);
}
protected virtual object GetMember(string propertyName)
{
return GetPropertyInfo(propertyName).GetValue(ProxiedObject, null);
}
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
protected virtual void RaisePropertyChanged(string propertyName)
{
OnPropertyChanged(propertyName);
}
#region constructor
public DynamicProxy()
{
}
public DynamicProxy(object proxiedObject)
{
ProxiedObject = proxiedObject;
mPropertyDescriptors = TypeDescriptor.GetProperties(ProxiedObject.GetType());
}
#endregion
public override bool TryConvert(ConvertBinder binder, out object result)
{
if (binder.Type == typeof (INotifyPropertyChanged))
{
result = this;
return true;
}
if (ProxiedObject != null && binder.Type.IsInstanceOfType(ProxiedObject))
{
result = ProxiedObject;
return true;
}
return base.TryConvert(binder, out result);
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
result = GetMember(binder.Name);
return true;
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
SetMember(binder.Name, value);
return true;
}
#region public properties
public object ProxiedObject { get; set; }
#endregion
#region INotifyPropertyChanged Member
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
假设我们有一个这样的域对象:
public class DomainObject
{
public string Name { get; set; }
}
而这个观点:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<TextBox
VerticalAlignment="Top"
Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}">
</TextBox>
</Grid>
</Window>
并像这样为其分配代理:
DataContext = new DynamicProxy(new DomainObject());
我有一个 WPF View
绑定到实现 INotifyPropertyChanged
的 ViewModel
。在 View
中,我有一个 DataGrid
,其项目绑定到 ViewModel
中的 ObservableCollection
。 ObservableCollection
成员自己没有实现 INotifyPropertyChanged
。在 DataGrid
中,我有三列 - 两个可编辑的 DatePicker
列和一个只读的 Checkbox
列,当用户选择包含当前日期的日期范围时,应该选中这些列。
绑定在首次加载视图时起作用,但当我更改其他两列中的日期时,Checkbox
的绑定未更新,大概是因为 class MyDomainObject
没有实施 INotifyPropertyChanged
。我真的不想在这里实现那个接口,因为 ObservableCollection
是从网络服务返回的类型,这样做会迫使我创建和维护该类型的副本,而且我有很多我的应用程序中的类似场景。有什么办法可以强制复选框更新吗?如果可能的话,我非常愿意避免使用代码隐藏——如果我必须打破那个规则,我知道该怎么做。
下面的代码应该可以大致说明问题:
XAML:
<DataGrid ItemsSource="{Binding MyDomainObjectCollection}">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Start Date">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding StartDate, StringFormat=d}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<DatePicker SelectedDate="{Binding StartDate, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="End Date">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding EndDate, StringFormat=d}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<DatePicker SelectedDate="{Binding EndDate, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Is Active">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsActive, Mode=OneWay}" IsEnabled="False" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
查看模型:
public class MyViewModel : INotifyPropertyChanged {
ObservableCollection<MyDomainObject> _myDomainObjectCollection;
public ObservableCollection<MyDomainObject> MyDomainObjectCollection {
get { return this._myDomainObjectCollection; }
set { this._myDomainObjectCollection = value; this.OnPropertyChanged(); }
}
[...]
}
域对象:
public class MyDomainObject {
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public bool IsActive {
get { return StartDate < DateTime.Now && EndDate > DateTime.Now; }
}
}
如果您不想在您的域对象上实现 INotifyPropertyChanged
接口,您可以使用与您的 DomainObject 具有相同接口并实现 INotifyPropertyChanged
public class MyDomainObjectViewModel : INotifyPropertyChanged {
private MyDomainObject _domainObject;
public MyDomainObjectViewModel(MyDomainObject domainObject){
_domainObject = domainObject;
}
public DateTime StartDate {
get{
return _domainObject.StartDate;
}
set{
_domainObject.StartDate = value;
RaisePropertyChanged("StartDate");
RaisePropertyChanged("IsActive");
}
}
public DateTime EndDate {
get{
return _domainObject.EndDate ;
}
set{
_domainObject.EndDate = value;
RaisePropertyChanged("EndDate");
RaisePropertyChanged("IsActive");
}
}
public bool IsActive {
get { return StartDate < DateTime.Now && EndDate > DateTime.Now; }
}
}
您的 ObservableCollection 将是 ObservableCollection<MyDomainObjectViewModel>
.
几年前我遇到过同样的问题。我找到的解决方案使用动态对象作为实现 INotifyPropertyChanged 的代理对象。它通过反射挂钩 getter 和 setter,并在代理更改时更新源对象。
public class DynamicProxy : DynamicObject, INotifyPropertyChanged
{
private readonly PropertyDescriptorCollection mPropertyDescriptors;
protected PropertyInfo GetPropertyInfo(string propertyName)
{
return
ProxiedObject.GetType()
.GetProperties()
.FirstOrDefault(propertyInfo => propertyInfo.Name == propertyName);
}
protected virtual void SetMember(string propertyName, object value)
{
var propertyInfo = GetPropertyInfo(propertyName);
if (propertyInfo.PropertyType == value.GetType())
{
propertyInfo.SetValue(ProxiedObject, value, null);
}
else
{
var underlyingType = Nullable.GetUnderlyingType(propertyInfo.PropertyType);
if (underlyingType != null)
{
var propertyDescriptor = mPropertyDescriptors.Find(propertyName, false);
var converter = propertyDescriptor.Converter;
if (converter != null && converter.CanConvertFrom(typeof (string)))
{
var convertedValue = converter.ConvertFrom(value);
propertyInfo.SetValue(ProxiedObject, convertedValue, null);
}
}
}
RaisePropertyChanged(propertyName);
}
protected virtual object GetMember(string propertyName)
{
return GetPropertyInfo(propertyName).GetValue(ProxiedObject, null);
}
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
protected virtual void RaisePropertyChanged(string propertyName)
{
OnPropertyChanged(propertyName);
}
#region constructor
public DynamicProxy()
{
}
public DynamicProxy(object proxiedObject)
{
ProxiedObject = proxiedObject;
mPropertyDescriptors = TypeDescriptor.GetProperties(ProxiedObject.GetType());
}
#endregion
public override bool TryConvert(ConvertBinder binder, out object result)
{
if (binder.Type == typeof (INotifyPropertyChanged))
{
result = this;
return true;
}
if (ProxiedObject != null && binder.Type.IsInstanceOfType(ProxiedObject))
{
result = ProxiedObject;
return true;
}
return base.TryConvert(binder, out result);
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
result = GetMember(binder.Name);
return true;
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
SetMember(binder.Name, value);
return true;
}
#region public properties
public object ProxiedObject { get; set; }
#endregion
#region INotifyPropertyChanged Member
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
假设我们有一个这样的域对象:
public class DomainObject
{
public string Name { get; set; }
}
而这个观点:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<TextBox
VerticalAlignment="Top"
Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}">
</TextBox>
</Grid>
</Window>
并像这样为其分配代理:
DataContext = new DynamicProxy(new DomainObject());