通过 IDictionary<string,object> 绑定 属性 更改的事件处理程序为空
Binding through IDictionary<string,object> property changed event handler is null
我有一个 class FormItem
实现了 IDictionary<string, object>
,当我绑定 FormItem
的 Value
属性 时, PropertyChanged
事件始终为空,但是当我从 FormItem
中删除 IDictionary
时,FormItem.PropertyChanged
事件不为空。我想知道为什么在我实施 IDictionary
时它为 null 以及如何修复它。
例子
public class FormItem : INotifyPropertyChanged, IDictionary<string, object>
{
public int _value;
public int Value
{
get { return _value; }
set
{
_value= value;
OnPropertyChanged("Value");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public bool CanEdit
{
get { return true; }
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
throw new NotImplementedException();
}
public void Add(KeyValuePair<string, object> item)
{
throw new NotImplementedException();
}
public void Clear()
{
throw new NotImplementedException();
}
public bool Contains(KeyValuePair<string, object> item)
{
throw new NotImplementedException();
}
public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
{
throw new NotImplementedException();
}
public bool Remove(KeyValuePair<string, object> item)
{
throw new NotImplementedException();
}
public int Count { get; }
public bool IsReadOnly { get; }
public void Add(string key, object value)
{
throw new NotImplementedException();
}
public bool ContainsKey(string key)
{
throw new NotImplementedException();
}
public bool Remove(string key)
{
throw new NotImplementedException();
}
public bool TryGetValue(string key, out object value)
{
throw new NotImplementedException();
}
public object this[string key]
{
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
public ICollection<string> Keys { get; }
public ICollection<object> Values { get; }
}
<Page
x:Class="App1.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:App1"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<TextBox x:Name="text" Text="{Binding FormItem.Val}"></TextBox>
<Button x:Name="button" Visibility="{Binding FormItem.CanEdit}" Content="Hello World" />
</Grid>
</Grid>
</Page>
MainPage.xaml.cs:
using Windows.UI.Xaml.Controls;
// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409
namespace App1
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
this.DataContext= new DataPageViewModel();
}
}
}
DataPageViewModel.cs:
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace App1
{
class DataPageViewModel : INotifyPropertyChanged
{
private FormItem _formItem;
public FormItem FormItem
{
get { return _formItem; }
set
{
_formItem = value;
OnPropertyChanged("FormItem");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public DataPageViewModel()
{
this.FormItem = new FormItem();
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
您看到的问题是 Microsoft 拒绝实现原始 WPF API 和 WinRT/UWP API(主要源自 Silverlight/Windows Phone API,WPF 的精简和改造版本 API)。我的猜测是,如果您询问 Microsoft,或者至少询问负责 UWP 的人员,他们会声称这是一项功能,而不是错误。
发生的事情是,当您的类型实现 IDictionary<TKey, TValue>
接口时,UWP 决定支持通过 属性 路径语法以及通常的索引器语法对字典进行索引。也就是说,虽然在 WPF 中索引字典需要编写类似 Text={Binding FormItem[SomeKey]}
的内容,但在 UWP 中您可以编写 Text={Binding FormItem.SomeKey}
。当然,这完全打破了对 class 中任何实际 属性 的绑定。一旦 class 实现了 IDictionary<TKey, TValue>
接口,您就无法访问 仅 个字典项。
更糟糕的是,当绑定到索引字典项时,UWP 不会费心去订阅 PropertyChanged
事件。在 WPF 中,它有点 hacky,但您可以使用 属性 的名称作为 "Item" 引发 PropertyChanged
事件,并且绑定到索引值(例如通过字典)将是刷新。 UWP 甚至不包括这种骇人听闻的行为。索引字典项实际上是一次性绑定。
如果是我,我不会在我想要访问其他属性的同一对象上实现字典接口。相反,更改模型层次结构,使您的模型对象 严格 模型对象。如果你在某处需要字典行为,让你的模型对象 contain 一个字典,而不是 be 一个字典。这将确保您将索引字典项和命名属性分开。
但是,在这种情况下还有另一种解决方法。 {x:Bind}
语法在设计上受到更多限制, 而不是 合并索引器和 属性 路径语法。缺点是它也不依赖于 DataContext
。但是,如果您愿意将 属性 添加到 Page
class,您可以使用 {x:Bind}
语法,这将如您所愿。例如:
public sealed partial class MainPage : Page
{
public DataPageViewModel Model { get { return (DataPageViewModel)DataContext; } }
public MainPage()
{
this.InitializeComponent();
this.DataContext = new DataPageViewModel();
}
// I added this to your example so that I had a way to modify
// the property and observe any binding updates
private void button_Click(object sender, RoutedEventArgs e)
{
Model.FormItem.Value++;
}
}
请注意,我将 DataContext
包裹在新的 属性 中。这允许您混合搭配 {Binding}
和 {x:Bind}
语法。
另请注意,您需要将 DataPageViewModel
class 更改为 public
或(我的偏好)更改 MainPage
class 不是是public
。否则,您将收到一个编译时错误,抱怨新 Model
属性.
上的可访问性不一致
然后,在 XAML:
<TextBox x:Name="text" Text="{x:Bind Model.FormItem.Value, Mode=TwoWay}"/>
注意{x:Bind}
默认是一次性绑定。要在 属性 更改时获取更新,您需要将模式明确设置为 OneWay
或 TwoWay
.
以这种方式实现您的视图,您可以保留混合模型+字典设计,并且仍然让 UWP 观察 属性 更改。
我有一个 class FormItem
实现了 IDictionary<string, object>
,当我绑定 FormItem
的 Value
属性 时, PropertyChanged
事件始终为空,但是当我从 FormItem
中删除 IDictionary
时,FormItem.PropertyChanged
事件不为空。我想知道为什么在我实施 IDictionary
时它为 null 以及如何修复它。
例子
public class FormItem : INotifyPropertyChanged, IDictionary<string, object>
{
public int _value;
public int Value
{
get { return _value; }
set
{
_value= value;
OnPropertyChanged("Value");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public bool CanEdit
{
get { return true; }
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
throw new NotImplementedException();
}
public void Add(KeyValuePair<string, object> item)
{
throw new NotImplementedException();
}
public void Clear()
{
throw new NotImplementedException();
}
public bool Contains(KeyValuePair<string, object> item)
{
throw new NotImplementedException();
}
public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
{
throw new NotImplementedException();
}
public bool Remove(KeyValuePair<string, object> item)
{
throw new NotImplementedException();
}
public int Count { get; }
public bool IsReadOnly { get; }
public void Add(string key, object value)
{
throw new NotImplementedException();
}
public bool ContainsKey(string key)
{
throw new NotImplementedException();
}
public bool Remove(string key)
{
throw new NotImplementedException();
}
public bool TryGetValue(string key, out object value)
{
throw new NotImplementedException();
}
public object this[string key]
{
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
public ICollection<string> Keys { get; }
public ICollection<object> Values { get; }
}
<Page
x:Class="App1.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:App1"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<TextBox x:Name="text" Text="{Binding FormItem.Val}"></TextBox>
<Button x:Name="button" Visibility="{Binding FormItem.CanEdit}" Content="Hello World" />
</Grid>
</Grid>
</Page>
MainPage.xaml.cs:
using Windows.UI.Xaml.Controls;
// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409
namespace App1
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
this.DataContext= new DataPageViewModel();
}
}
}
DataPageViewModel.cs:
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace App1
{
class DataPageViewModel : INotifyPropertyChanged
{
private FormItem _formItem;
public FormItem FormItem
{
get { return _formItem; }
set
{
_formItem = value;
OnPropertyChanged("FormItem");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public DataPageViewModel()
{
this.FormItem = new FormItem();
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
您看到的问题是 Microsoft 拒绝实现原始 WPF API 和 WinRT/UWP API(主要源自 Silverlight/Windows Phone API,WPF 的精简和改造版本 API)。我的猜测是,如果您询问 Microsoft,或者至少询问负责 UWP 的人员,他们会声称这是一项功能,而不是错误。
发生的事情是,当您的类型实现 IDictionary<TKey, TValue>
接口时,UWP 决定支持通过 属性 路径语法以及通常的索引器语法对字典进行索引。也就是说,虽然在 WPF 中索引字典需要编写类似 Text={Binding FormItem[SomeKey]}
的内容,但在 UWP 中您可以编写 Text={Binding FormItem.SomeKey}
。当然,这完全打破了对 class 中任何实际 属性 的绑定。一旦 class 实现了 IDictionary<TKey, TValue>
接口,您就无法访问 仅 个字典项。
更糟糕的是,当绑定到索引字典项时,UWP 不会费心去订阅 PropertyChanged
事件。在 WPF 中,它有点 hacky,但您可以使用 属性 的名称作为 "Item" 引发 PropertyChanged
事件,并且绑定到索引值(例如通过字典)将是刷新。 UWP 甚至不包括这种骇人听闻的行为。索引字典项实际上是一次性绑定。
如果是我,我不会在我想要访问其他属性的同一对象上实现字典接口。相反,更改模型层次结构,使您的模型对象 严格 模型对象。如果你在某处需要字典行为,让你的模型对象 contain 一个字典,而不是 be 一个字典。这将确保您将索引字典项和命名属性分开。
但是,在这种情况下还有另一种解决方法。 {x:Bind}
语法在设计上受到更多限制, 而不是 合并索引器和 属性 路径语法。缺点是它也不依赖于 DataContext
。但是,如果您愿意将 属性 添加到 Page
class,您可以使用 {x:Bind}
语法,这将如您所愿。例如:
public sealed partial class MainPage : Page
{
public DataPageViewModel Model { get { return (DataPageViewModel)DataContext; } }
public MainPage()
{
this.InitializeComponent();
this.DataContext = new DataPageViewModel();
}
// I added this to your example so that I had a way to modify
// the property and observe any binding updates
private void button_Click(object sender, RoutedEventArgs e)
{
Model.FormItem.Value++;
}
}
请注意,我将 DataContext
包裹在新的 属性 中。这允许您混合搭配 {Binding}
和 {x:Bind}
语法。
另请注意,您需要将 DataPageViewModel
class 更改为 public
或(我的偏好)更改 MainPage
class 不是是public
。否则,您将收到一个编译时错误,抱怨新 Model
属性.
然后,在 XAML:
<TextBox x:Name="text" Text="{x:Bind Model.FormItem.Value, Mode=TwoWay}"/>
注意{x:Bind}
默认是一次性绑定。要在 属性 更改时获取更新,您需要将模式明确设置为 OneWay
或 TwoWay
.
以这种方式实现您的视图,您可以保留混合模型+字典设计,并且仍然让 UWP 观察 属性 更改。