How to implement INotifyPropertyChanged on indexed child in SL4?
My ViewModel class has a child property of type 'Messages' that has an indexed property, like:
public class ViewModel
{
// ...
public Messages Messages
{
get
{
if (_messages == null)
{
LoadMessagesAsync();
_messages = new Messages();
}
return _messages;
}
set
{
_messages = values;
PropertyChanged(new PropertyChangedArgs("Messages");
}
}
// ...
开发者_Go百科 private void LoadMessagesAsync()
{
// Do the service call
Messages = theResult;
}
}
public class Messages
{
// ...
public String this[String name]
{
get { return _innerDictionary[name]; }
}
// ...
}
I don't think I need to fill in the rest of the gaps as it is all straight-forward.
The problem I am having is that the binding is not updating when I set the Messages property to a new object. Here is how I am referencing the property in XAML (with ViewModel as the DataContext):
<TextBlock Text="{Binding Messages[HelloWorld]}" />
It was my assumption that the binding would update when the PropertyChanged event was raised for the "Messages" property.
I've read elsewhere that my Messages class should raise a PropertyChanged event with either an empty string (""), "Item[]" or "Item["+name+"]" for the property name. However, since I am completely replacing the Messages object, this won't work as I never actually change the contents.
How do I make this work?
UPDATE
So I've done some digging into the behavior and into the BCL source code to see what's expected as a way to figure out how to make my code work. What I've learned is two-fold:
First, Silverlight data-binding is actually looking at the return object from the Messages property as the source of the binding. So raising PropertyChanged from ViewModel (sender is ViewModel) is not handled by the binding. I actually have to raise the event from the Messages class.
This is no different than using the following: Text={Binding Messages.HelloWorld}"
The reason that Myles' code work is that 'Data' returns 'this' so the binding is fooled into treating the parent class as the binding source.
That said, even if I make it so my child object raises the event, it still won't work. This is because the binding uses the System.Windows.IndexerListener as the binding target. In the SourcePropertyChanged method, the listener checks if the property name is "Item[]" but takes no action. The next statement delegates to the PropertyListener which checks the property name and only handles the event if it is equal to "Item[HelloWorld]".
So, unless I explicitly raise the event for each possible value within my collection, the UI will never update. This is disappointing because other articles and posts indicate that "Item[]" should work but looking at the source proves otherwise.
Nevertheless, I still hold out hope that there is a way to accomplish my goals.
OK the underlying problem here is that the Binding does not have a Path specified, therefore, the binding framework does not know which property name to look out for when handling PropertyChanged events. So I have fabricated a Path for the binding in order for change notification to work.
I have wrote the following code that proves the indexer binding is refreshed when the actual underlying dictionary changes:
ViewModel
public class BindingTestViewModel : AppViewModelBase, IBindingTestViewModel
{
private Dictionary<string, object> _data = new Dictionary<string, object>();
public BindingTestViewModel()
{
_data.Add("test","1");
_data.Add("test2", "21");
}
public object this[string index]
{
get
{
return _data[index];
}
set
{
_data[index] = value;
NotifyOfPropertyChange(() => Data);
}
}
public object Data
{
get
{
return this;
}
}
public void Refresh()
{
_data = new Dictionary<string, object>
{
{"test", "2"}, {"test2", "22"}
};
NotifyOfPropertyChange(() => Data);
}
}
My XAML:
<navigation:Page.Resources>
<Converters:IndexConverter x:Name="IndexConverter"></Converters:IndexConverter>
</navigation:Page.Resources>
<Grid x:Name="LayoutRoot">
<TextBox Height="23"
HorizontalAlignment="Left"
Margin="288,206,0,0"
Name="textBox1"
Text="{Binding Path=Data,Converter={StaticResource IndexConverter},ConverterParameter=test}"
VerticalAlignment="Top" Width="120" />
<Button x:Name="ReloadDict" Click="ReloadDict_Click" Width="50" Height="30" Content="Refresh" VerticalAlignment="Top"></Button>
</Grid>
The converter:
public class IndexConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
var vm = value as BindingTestViewModel;
var index = parameter as string;
return vm[index];
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
I don't necessarily like answering my own questions but I have found a solution to my problem. I've been able to keep my original flow and address the idiosyncrocies of SL4 data-binding. And the code seems a bit cleaner, too.
What it boils down to is that I don't replace the child object anymore. That seems to be the key. Instead, I create a single instance and let that instance manage changing the internal list of items as needed. The child object notifies the parent when it has changed so the parent can raise the PropertyChanged event. The following is a brief example how I've gotten it to work:
public class ViewModel
{
// ...
public Messages Messages
{
get
{
if (_messages == null)
{
lock (_messagesLock)
{
if (_messages == null)
{
_messages = new Messages();
_messages.ListChanged += (s, e) =>
{
NotifyPropertyChanged("Messages");
};
}
}
}
return _messages;
}
}
}
public class Messages
{
// ...
public String this[String name]
{
get
{
if (_innerDictionary == null)
{
_innerDictionary = new Dictionary<String, String>();
LoadMessagesAsync();
}
return _innerDictionary[name];
}
}
// ...
private void LoadMessagesAsync()
{
// Do the service call
_innerDictionary = theResult;
NotifyListChanged();
}
// ...
public event EventHandler ListChanged;
}
For brevity, I've left out the obvious parts.
精彩评论