Sharing DataContext to UserControl in a type safe way
I'm developing my first Silverlight 4 app and are struggling on how to to share my DataContext set on the top element (a Grid) in my MainPage.xaml into an underlying UserControl, in a type safe way. The DataContext is an instance of my ViewModel class and my thoug开发者_开发问答ht is to be able to bind certain elements in the UserControl to properties of the ViewModel.
I am pretty sure the ViewModel object bubbles down to my UserControl but how can I in the UserControl asure that the DataContext is of type PatternCreatorViewModel?
Hope this was understandable!
This is (in my lonely opinion) one of the biggest limitations of the data binding model in Silverlight and WPF, namely, that there's no type safety anywhere in the process. As soon as you type {Binding...} you're working without a net. MS managed to take a wonderfully glorious strongly-typed language like C# and tied it to a completely non-type-safe data binding model, thereby all but wrecking a decade of Anders Hejlsberg's wonderful work on C#. You expect this sort of "looseness" when working with dynamic languages, but not when you're dealing with C#.
This limitation really becomes problematic when you're changing the ViewModel underlying your Views, because of course, there's no easy way to test your data bindings. Normally, when you've got code that you can't test, you can at least rely on the compiler to tell you if what you're asking the code to do doesn't make any sense. But because MS made data bindings non-type-safe, not only can you not test your changes, you can't even rely on the compiler to tell you when they don't make any sense. And, to add insult to injury, you can't even rely on running your application and seeing if you get any error messages: because bindings always fail silently. The best you can do is turn up the logging level and walk through tons of debug error messages. Uggh. Nasty as hell.
See my blog posting here, another question I asked here, and my answer here for more thoughts on the underlying issue.
I should note that I seem to be virtually alone in my opinion about this one, so perhaps there's something huge that I'm just missing. But I personally think you've hit the nail right on the head.
Pleasd see update below: the first proposed solution may cause threading-issues.
One possibility is creating a DependencyProperty
of the required type, updating that during DataContextChanged
and binding to that.
DefaultEditor.xaml.cs:
public partial class DefaultEditor : UserControl
{
public DefaultEditor()
{
this.DataContextChanged += OnDataContextChanged;
InitializeComponent();
}
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
SetValue(propertyNameProperty, this.DataContext as IPropertyProvider);
}
public static readonly DependencyProperty propertyNameProperty = DependencyProperty.Register(
nameof(PropertyProvider), typeof(IPropertyProvider), typeof(DefaultEditor), new PropertyMetadata(default(IPropertyProvider)));
public IPropertyProvider PropertyProvider
{
get { return (IPropertyProvider)GetValue(propertyNameProperty); }
}
}
Note: I reduced the standard Visual Studio pattern for the DepenendencyProperty, because I did not want a public setter.
Also, make sure to add the event handler before calling InitializeComponent()
.
DefaultEditor.xaml:
<UserControl x:Class="MyApp.DefaultEditor" x:Name="self">
<Grid DataContext="{Binding ElementName=self, Path=PropertyProvider}">
<StackPanel>
<TextBlock Text="{Binding Index}"/>
<TextBlock Text="{Binding Name}"/>
</StackPanel>
</Grid>
</UserControl>
Once x:Name="self"
is defined I have Intellisense for Path=PropertyProvider
and the other bindings.
Note: Be sure not to set the entire control's DataContext
(which would be recursive), use another (top-level) element like the Grid
.
And, in case it wasn't obvious: In the example above IPropertyProvider
is a custom type that must be replaced with the required type.
UPDATE:
While the above does work, it can invite code to access DefaultEditor.PropertyProvider
from a wrong thread (other than the element's dispatcher's thread), which leads to an InvalidOperationException
. This can be resolved by replacing the DependencyProperty
with a simple property and implementing INotifyPropertyChanged
.
Here's an updated code-behind, the .xaml remains the same.
DefaultEditor.xaml.cs
public partial class DefaultEditor : UserControl, INotifyPropertyChanged
{
public DefaultEditor()
{
this.DataContextChanged += OnDataContextChanged;
InitializeComponent();
}
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
PropertyProvider = this.DataContext as IPropertyProvider;
}
private IPropertyProvider _propertyProvider;
public IPropertyProvider PropertyProvider { get => _propertyProvider; private set => SetField(ref _propertyProvider, value); }
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
#endregion
}
Note: The INotifyPropertyChanged
implementation is a Visual Studio (Resharper?) code-snippet.
Using this code, DefaultEditor.PropertyProvider
can be accessed from any thread.
精彩评论