问题
I have a class FormItem
which implements an IDictionary<string, object>
, when I bind the Value
property of the FormItem
, the PropertyChanged
event is always null, but when I remove the IDictionary
from FormItem
then the FormItem.PropertyChanged
event is not null. I want to know why it is null when I am implementing IDictionary
and how it can be fixed.
Example
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));
}
}
}
回答1:
The issue you are seeing is an unfortunate consequence of Microsoft's refusal to achieve parity between the original WPF API and the WinRT/UWP API (which is derived primarily from the Silverlight/Windows Phone API, a dumbed-down and reinvented version of the WPF API). My guess is that if you asked Microsoft, or at least the folks responsible for UWP, they would claim this as a feature, not a bug.
What's going on is that when your type implements the IDictionary<TKey, TValue>
interface, UWP decides to support indexing the dictionary via the property path syntax as well as the usual indexer syntax. That is, while in WPF indexing the dictionary would require writing something like Text={Binding FormItem[SomeKey]}
, in UWP you can write Text={Binding FormItem.SomeKey}
. Of course, this completely breaks binding to any actual properties in the class. Once the class implements the IDictionary<TKey, TValue>
interface, you're stuck being able to access only dictionary items.
To make matters worse, when binding to an indexed dictionary item, UWP does not bother to subscribe to the PropertyChanged
event. In WPF, it's a bit hacky, but you can raise the PropertyChanged
event with the name of the property as "Item", and bindings to indexed values (such as through a dictionary) will be refreshed. UWP doesn't include even that hacky behavior. Indexed dictionary items are de facto one-time bindings.
If it were me, I would just not implement the dictionary interface on the same object where I wanted access to other properties. Instead, change the model hierarchy so that your model objects are strictly model objects. If you need dictionary behavior somewhere, make your model objects contain a dictionary, rather than be a dictionary. This will ensure you keep indexed dictionary items and named properties separate.
However, there is another work-around in this case. The {x:Bind}
syntax, being more limited by design, does not conflate indexer and property path syntax. The down-side is that it also does not rely on DataContext
. But, if you're willing to add a property to your Page
class, you can use the {x:Bind}
syntax and that will work as you expect. For example:
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++;
}
}
Note that I wrapped the DataContext
in the new property. This allows you to mix-and-match {Binding}
and {x:Bind}
syntax.
Note also that you'll either need to change your DataPageViewModel
class to be public
or (my preference) change your MainPage
class to not be public
. Otherwise, you'll get a compile-time error complaining about inconsistent accessibility on the new Model
property.
Then, in the XAML:
<TextBox x:Name="text" Text="{x:Bind Model.FormItem.Value, Mode=TwoWay}"/>
Note that {x:Bind}
is one-time binding by default. To get updates when the property changes, you need to explicitly set the mode to OneWay
or TwoWay
.
With your view implemented this way, you can keep the mixed model+dictionary design and still have UWP observe property changes.
来源:https://stackoverflow.com/questions/46997601/binding-through-idictionarystring-object-property-changed-event-handler-is-nul