WPF project + Prism 7 + (Pure MVVM pattern)
Simple, I have TextBox which need to be cleared when some button is pressed (without the violation to the MVVM pattern)
<Button Command="{Binding ClearCommand}"/>
<TextBox Text="{Binding File}">
<i:Interaction.Behaviors>
<local:ClearTextBehavior ClearTextCommand="{Binding ClearCommand, Mode=OneWayToSource}" />
</i:Interaction.Behaviors>
</TextBox>
ViewModel
public class ViewModel {
public ICommand ClearCommand { get; set; }
}
Behavior
public class ClearTextBehavior : Behavior<TextBox>
{
public ICommand ClearTextCommand
{
get { return (ICommand)GetValue(ClearTextCommandProperty); }
set
{
SetValue(ClearTextCommandProperty, value);
RaisePropertyChanged();
}
}
public static readonly DependencyProperty ClearTextCommandProperty =
DependencyProperty.Register(nameof(ClearTextCommand), typeof(ICommand), typeof(ClearTextBehavior));
public ClearTextBehavior()
{
ClearTextCommand = new DelegateCommand(ClearTextCommandExecuted);
}
private void ClearTextCommandExecuted()
{
this.AssociatedObject.Clear();
}
}
The problem is the command in the ViewModel is always null (it did not bound to the command in the Behavior), Although I made sure that it is initialized in the behavior class.
NOTE: please do NOT suggest to set the File property to empty string, because this is just an example, In my real case, I need to select all the Text, so I really need an access to the AssociatedObject of the behavior
If i understood your Question correctly, you want to know why the ICommand in the ViewModel is not set to the DelegateCommand defined in the Behaviour.
The Problem is, that the ICommand and the DelegateCommand do not have a direct connection. I assume you may misunderstood how a Binding works and what happens by using those.
First of all, the ICommand is 'comes' from a Class and is therefore a reference Type.
Second, the reference to the ICommand is saved within the DependencyProperty ClearTextCommandProperty.
Third, by using a Binding in the XAML something like this happens as C# code:
Binding binding = new Binding();
binding.Path = new PropertyPath("ClearTextCommand");
binding.Source = ClearCommand;
BindingOperations.SetBinding(TextBox.ClearTextCommandProperty, binding);
Now the important thing: I don't know exactly which assignment comes first, but both lines will override the Value reference in the ClearTextCommandProperty!
//either here
SetBinding(TextBox.ClearTextCommandProperty, binding);
//or here
ClearTextCommand = new DelegateCommand(ClearTextCommandExecuted);
//Which could be written as
SetValue(ClearTextCommandProperty, new DelegateCommand(ClearTextCommandExecuted));
At no point there is an assignment like this:
ViewModel.ClearCommand = SomeICommand;
Therefore it is Null, as #Andy mentioned
Edited to match select all Text
Additionally, i suggest you drop this complex stuff and use the full potential of the Interactivity Package like this:
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
<Button>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<utils:SelectAllText TargetName="TextToSelect"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
<TextBox x:Name="TextToSelect" Text="{Binding File}"/>
And the SelectAllText
public class SelectAllText : TargetedTriggerAction<TextBox>
{
protected override void Invoke(object parameter)
{
if (Target == null) return;
Target.SelectAll();
Target.Focus();
}
}
If you take a look at this sample here:
https://social.technet.microsoft.com/wiki/contents/articles/31915.wpf-mvvm-step-by-step-1.aspx
You will notice that I have an ICommand there and it's set up to run a method.
If it was just an ICommand with a Get and Set like you have there then it would be NULL. There's a property but it's null until it is set to something.
This a very clunky way to implement an ICommand but relies on no external libraries or anything.
If you take a look at the second article in that series, it uses mvvmlight and relaycommand so creating a command is rather less clunky.
https://social.technet.microsoft.com/wiki/contents/articles/32164.wpf-mvvm-step-by-step-2.aspx
public RelayCommand AddListCommand { get; set; }
public MainWindowViewModel()
{
AddListCommand = new RelayCommand(AddList);
}
private void AddList()
{
stringList.Add(myString));
}
If you look at that code AddListCommand is initially null.
It is set in the constructor to a new RelayCommand which means it is then not null.
This is fairly simple but the code for the command is in a different place to the property so a more elegant approach is usual. As shown here: https://msdn.microsoft.com/en-gb/magazine/dn237302.aspx
Having said all that.
Selecting all text is something to do in the view, not the viewmodel.
You shouldn't really be passing a piece of UI from the view into a viewmodel.
Rather than a command it could well be that you should be binding a bool which is set in the viewmodel and acted on in the behaviour.
Related
I am building a WPF application that as part of its flow, checks for network connectivity and display the IP address in a TextBlock.
Now I am trying to update the TextBlock Text property everytime the IP address changes for whatever reason.
I have the IP address change working fine, but i could not get INotifyPropertyChanged to work.
I read all the possible solutions and implementations but I couldn't come up with a working code.
The public property gets the value from a static string from the Network Helper class.
So, the code:
public partial class MainWindow : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
}
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
public string ipAddress
{
get { return NetworkStatus.localIP; }
set
{
if (value != NetworkStatus.localIP)
{
NetworkStatus.localIP = value;
NotifyIPChanged("IpAddress");
}
}
}
private void NotifyIPChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
XAML:
<TextBlock x:Name="ipTxt"
TextWrapping="Wrap"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding DataContext.ipAddress}"
Height="30"
Width="110"
Margin="-30,10,0,-10"
/>
UPDATE
NetWorkStatus.cs -- static bool IsNetworkAvailable()
...
if (statistics.BytesReceived > 0 || statistics.BytesSent > 0)
{
IPHostEntry host = Dns.GetHostEntry(Dns.GetHostName());
localIP = host.AddressList.FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork).ToString();
return true;
}
As you can see this method sets a static string "localIP". This is then evaluated by IpAddress property.
Why the TextBlock Text property doesn't get updated when the IP Address changes?
Rename the property to IpAddress so that it adheres to widely accepted naming conventions.
public string IpAddress
{
get { return NetworkStatus.localIP; }
set
{
if (value != NetworkStatus.localIP)
{
NetworkStatus.localIP = value;
NotifyPropertyChanged();
}
}
Use the CallerMemberName attribute on the propertyName parameter of your notification method, so that you do not have to write the name explicitly.
using System.Runtime.CompilerServices;
...
private void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Bind it correctly. The current DataContext is already used as source object of the Binding. You must not add it to the property path.
<TextBlock Text="{Binding IpAddress}" ... />
In a possible next step you might want to separate the view from the view model and put the property in a separate class:
public class ViewModel : INotifyPropertyChanged
{
public string IpAddress
{
get ...
set ...
}
...
}
and assign the Window's DataContext to an instance of the view model class:
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
I think you need to take a closer look to how WPF works.
As a remark, there is no need to implement INotifyPropertyChanged in code behind. If you are using events, then you can automatically refresh the properties of the targeted UI element.
However, using code behind is not a good practice in our days. You should take a look at MVVM pattern. You have there a Model, View and ViewModel. The ViewModel should implement INotifyPropertyChanged.
The fact is that your code is in my opinion absolutely wrong. The naming is not fine : when you implement INotifyPropertyChanged you should not implement it only for a property, and the name should not look like : NotifyIPChanged, instead you should use RaisePropertyChanged, NotifyPropertyChanged or OnPropertyChanged. In setters you should not refresh something else, but only the property that you are targeting, because otherwise the Single Responsability principle is violated, as in your case. Also a bad practice is to bind to Code Behind.
I hope this post would make you to read more about MVVM and WPF. Good luck!
is it possible that the Event doesnt react, because the first letter of IpAdress is an Upper?
NotifyIPChanged("IpAddress");
public string ipAddress
Text="{Binding DataContext.ipAddress}"
I understand that ViewModel shouldn't have any knowledge of View, but how can I call MediaElement.Play() method from ViewModel, other than having a reference to View (or directly to MediaElement) in ViewModel?
Other (linked) question: how can I manage View's controls visibility from ViewModel without violating MVVM pattern?
1) Do not call Play() from the view model. Raise an event in the view model instead (for instance PlayRequested) and listen to this event in the view:
view model:
public event EventHandler PlayRequested;
...
if (this.PlayRequested != null)
{
this.PlayRequested(this, EventArgs.Empty);
}
view:
ViewModel vm = new ViewModel();
this.DataContext = vm;
vm.PlayRequested += (sender, e) =>
{
this.myMediaElement.Play();
};
2) You can expose in the view model a public boolean property, and bind the Visibility property of your controls to this property. As Visibility is of type Visibility and not bool, you'll have to use a converter.
You can find a basic implementation of such a converter here.
This related question might help you too.
For all the late-comers,
There are many ways to achieve the same result and it really depends on how you would like to implement yours, as long as your code is not difficult to maintain, I do believe it's ok to break the MVVM pattern under certain cases.
But having said that, I also believe there is always way to do this within the pattern, and the following is one of them just in case if anyone would like to know what other alternatives are available.
The Tasks:
we don't want to have direct reference from the ViewModel to any UI elements, i.e. the the MediaElement and the View itself.
we want to use Command to do the magic here
The Solution:
In short, we are going to introduce an interface between the View and the ViewModel to break the dependecy, and the View will be implementing the interface and be responsible for the direct controlling of the MediaElement while leaving the ViewModel talking only to the interface, which can be swapped with other implementation for testing purposes if needed, and here comes the long version:
Introduce an interface called IMediaService as below:
public interface IMediaService
{
void Play();
void Pause();
void Stop();
void Rewind();
void FastForward();
}
Implement the IMediaService in the View:
public partial class DemoView : UserControl, IMediaService
{
public DemoView()
{
InitializeComponent();
}
void IMediaService.FastForward()
{
this.MediaPlayer.Position += TimeSpan.FromSeconds(10);
}
void IMediaService.Pause()
{
this.MediaPlayer.Pause();
}
void IMediaService.Play()
{
this.MediaPlayer.Play();
}
void IMediaService.Rewind()
{
this.MediaPlayer.Position -= TimeSpan.FromSeconds(10);
}
void IMediaService.Stop()
{
this.MediaPlayer.Stop();
}
}
we then do few things in the DemoView.XAML:
Give the MediaElement a name so the code behind can access it like above:
<MediaElement Source="{Binding CurrentMedia}" x:Name="MediaPlayer"/>
Give the view a name so we can pass it as a parameter, and
import the interactivity namespace for later use (some default namespaces are omitted for simplicity reason):
<UserControl x:Class="Test.DemoView"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ia="http://schemas.microsoft.com/expression/2010/interactivity"
x:Name="MediaService">
Hookup the Loaded event through Trigger to pass the view itself to the view model through a Command
<ia:Interaction.Triggers>
<ia:EventTrigger EventName="Loaded">
<ia:InvokeCommandAction Command="{Binding LoadedCommand}" CommandParameter="{Binding ElementName=MediaService}"></ia:InvokeCommandAction>
</ia:EventTrigger>
</ia:Interaction.Triggers>
last but not least, we need to hookup the media controls through Commands:
<Button Command="{Binding PlayCommand}" Content="Play"></Button>
<Button Command="{Binding PauseCommand}" Content="Pause"></Button>
<Button Command="{Binding StopCommand}" Content="Stop"></Button>
<Button Command="{Binding RewindCommand}" Content="Rewind"></Button>
<Button Command="{Binding FastForwardCommand}" Content="FastForward"></Button>
We now can catch everything in the ViewModel (I'm using prism's DelegateCommand here):
public class AboutUsViewModel : SkinTalkViewModelBase, IConfirmNavigationRequest
{
public IMediaService {get; private set;}
private DelegateCommand<IMediaService> loadedCommand;
public DelegateCommand<IMediaService> LoadedCommand
{
get
{
if (this.loadedCommand == null)
{
this.loadedCommand = new DelegateCommand<IMediaService>((mediaService) =>
{
this.MediaService = mediaService;
});
}
return loadedCommand;
}
}
private DelegateCommand playCommand;
public DelegateCommand PlayCommand
{
get
{
if (this.playCommand == null)
{
this.playCommand = new DelegateCommand(() =>
{
this.MediaService.Play();
});
}
return playCommand;
}
}
.
. // other commands are not listed, but you get the idea
.
}
Side note: I use Prism's Auto Wiring feature to link up the View and ViewModel. So at the View's code behind file there is no DataContext assignment code, and I prefer to keep it that way, and hence I chose to use purely Commands to achieve this result.
I use media element to play sounds in UI whenever an event occurs in the application. The view model handling this, was created with a Source property of type Uri (with notify property changed, but you already know you need that to notify UI).
All you have to do whenever source changes (and this is up to you), is to set the source property to null (this is why Source property should be Uri and not string, MediaElement will naturally throw exception, NotSupportedException I think), then set it to whatever URI you want.
Probably, the most important aspect of this tip is that you have to set MediaElement's property LoadedBehaviour to Play in XAML of your view. Hopefully no code behind is needed for what you want to achieve.
The trick is extremely simple so I won't post a complete example. The view model's play function should look like this:
private void PlaySomething(string fileUri)
{
if (string.IsNullOrWhiteSpace(fileUri))
return;
// HACK for MediaElement: to force it to play a new source, set source to null then put the real source URI.
this.Source = null;
this.Source = new Uri(fileUri);
}
Here is the Source property, nothing special about it:
#region Source property
/// <summary>
/// Stores Source value.
/// </summary>
private Uri _Source = null;
/// <summary>
/// Gets or sets file URI to play.
/// </summary>
public Uri Source
{
get { return this._Source; }
private set
{
if (this._Source != value)
{
this._Source = value;
this.RaisePropertyChanged("Source");
}
}
}
#endregion Source property
As for Visibility, and stuff like this, you can use converters (e.g. from bool to visibility, which you can find on CodePlex for WPF, SL, WP7,8) and bind your control's property to that of the view model's (e.g. IsVisible). This way, you control parts of you view's aspect. Or you can just have Visibility property typed System.Windows.Visibility on your view model (I don't see any pattern breach here). Really, it's not that uncommon.
Good luck,
Andrei
P.S. I have to mention that .NET 4.5 is the version where I tested this, but I think it should work on other versions as well.
I have created a few Custom Controls (NOT UserControls) with bind-able "ClearCommand" ICommand dependency properties. This property will do exactly what it sounds: it will clear all the values from the control (textboxes, etc). I also bind (some) of those same properties to the VM I describe below.
Now I'm stuck trying to trigger the ClearCommand in those controls in the following MVVM scenario:
I've added a few such controls into my View. The View also includes a "Save" button that binds to my ViewModel's SaveCommand DelegateCommand property.
What I need to happen is that, upon a successful save, the VM should trigger the ClearCommand on those controls found in the View.
UPDATE
I've added code examples below. I have a few controls that resemble the ExampleCustomControl. Also, just to note, I am open to restructuring some of this if it's completely off.
Example Control snippet:
public class ExampleCustomControl : Control {
public string SearchTextBox { get; set; }
public IEnumerable<CustomObject> ResultList { get; set; }
public ExampleCustomControl() {
ClearCommand = new DelegateCommand(Clear);
}
/// <summary>
/// Dependency Property for Datagrid ItemSource.
/// </summary>
public static DependencyProperty SelectedItemProperty = DependencyProperty.Register("SelectedItem",
typeof(CustomObject), typeof(ExampleCustomControl), new PropertyMetadata(default(CustomObject)));
public CustomObject SelectedItem {
get { return (CustomObject)GetValue(SelectedCustomObjectProperty); }
set { SetValue(SelectedCustomObjectProperty, value); }
}
public static DependencyProperty ClearCommandProperty = DependencyProperty.Register("ClearCommand", typeof(ICommand),
typeof(ExampleCustomControl), new PropertyMetadata(default(ICommand)));
/// <summary>
/// Dependency Property for resetting the control
/// </summary>
[Description("The command that clears the control"), Category("Common Properties")]
public ICommand ClearCommand {
get { return (ICommand)GetValue(ClearCommandProperty); }
set { SetValue(ClearCommandProperty, value); }
}
public void Clear(object o) {
SearchTextBox = string.Empty;
SelectedItem = null;
ResultList = null;
}
}
Example View snippet:
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="30"/>
</Grid.RowDefinitions>
<control:ExampleCustomControl Grid.Row="0"
SelectedItem="{Binding Selection, UpdateSourceTrigger=PropertyChanged}" />
<Button Grid.Row="1" x:Name="ResetButton" Command="{Binding SaveCommand}">
Save
</Button>
</Grid>
Example ViewModel:
public class TestViewModel : WorkspaceTask {
public TestViewModel() {
View = new TestView { Model = this };
SaveCommand = new DelegateCommand(Save);
}
private CustomObject _selection;
public CustomObject Selection {
get { return _selection; }
set {
_selection = value;
OnPropertyChanged("Selection");
}
}
public DelegateCommand SaveCommand { get; private set; }
private void Save(object o) {
// perform save
// clear controls
}
}
As others have said the VM shouldn't know about the view directly in MVVM so it doesn't make sense really that the VM triggers something on your custom control to clear everything.
I would have set the DataContext of the custom control to an object that has all the properties you want to clear, which are all each bound (two-way) to your textboxes etc. Then in the Save() method you can set a new object (which the custom control DataContext is bound to) and all the properties will be cleared for you (assuming you have implemented INotifyPropertyChanged on the object).
UPDATED:
As per my comment, see an example of the workaround for your current setup (untested btw):
public static DependencyProperty SelectedItemProperty = DependencyProperty.Register("SelectedItem",
typeof(CustomObject), typeof(ExampleCustomControl), new PropertyMetadata(default(CustomObject), OnSelectedItemChanged));
private static void OnSelectedItemChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
var cont = source as ExampleCustomControl;
//do all the clearing of txtboxes etc here....
cont.SearchTextBox = string.Empty;
}
But I would still try and move all this into the VM. i.e. have a clear command, like you do with the save command and bind the textbox text etc to a property in the VM and when the command is called it clears everything, which you can then easily call from the Save method in the VM too. But obviously I have no idea what you are trying to achieve in the long run or how selectedItem and the textboxes etc are related, so depends (as always) i guess.
It sounds like you are thinking about this the wrong way. In MVVM the ViewModel should never know anything about the custom controls (hence you are having a problem with this Clear functionality).
Your requirements are a bit vague, but have you considered:
1) If the properties are bound from the VM, can't the Control detect when these are changed?
2) If you really need to call Clear from the XAML layer and want to keep it pure MVVM, then consider something like the Expression Blend SDK's CallMethodAction.
As a followup to my comment. I suspect your command is targeting the View and clearing the TextBoxes directly. Instead, have your command target the ViewModel and clear the properties the View is bound to. Then you can have the command be a property on the ViewModel and call it whenever needed.
The application I'm currently writing is using MVVM with the ViewModel-first pattern. I have XAML similar to the following:
<ContentControl Content="{Binding FooViewModel.BarViewModel.View, Mode=OneWay}"/>
Every VM is a DependencyObject. Every property is a DependencyProperty. Depending upon the state of the application, the value of the BarViewModel property of the FooViewModel can change, thus changing the value of the View property. Unfortunately when this happens, the new view is not displayed, and the old one remains.
This is extremely frustrating. I thought that if any part of a path expression changed, the binding would update, but that doesn't appear to be the case. When I've used shallower path expressions, such as FooViewModel.View and I've changed the value of the FooViewModel property, that has updated the ContentControl to which it's bound, but not in this case.
If your solution is that I abandon ViewModel-first, that is not an option, though I appreciate your advice. I must get this working as is.
CLARIFICATION
This is a question about data binding, and not about MVVM or how to implement it. You can safely ignore the MVVM aspects of this if it helps you to think about the problem, or if you have a different idea about how MVVM should be implemented. This is a large, existing project in which the MVVM design pattern cannot be changed. (It is far too late for that.)
So, with that said, the correct question to be answering is the following:
Given a binding path expression in which every element is a DependencyProperty and the final property is a view bound to a ContentControl, why does a change in a property in the middle of the path not cause the binding to update?
Although I would expect this to work, there are several problems with your approach.
Firstly, your view models should not use DependencyObject or DependencyProperty, this ties them in to WPF. They should instead implement INotifyPropertyChanged. This makes your view models reusable in other presentation technologies such as Silverlight.
Secondly, your view models shouldn't have references to your views, so you shouldn't require a View property on your view models.
I would seriously consider using an MVVM framework for view composition - Caliburn.Micro, for example, makes view model first development extremely straightforward, and already provides a view model base class which implements INotifyPropertyChanged, and a mechanism for building view compositions with conventions.
I.e. you can have a conductor view model which has an ActiveItem property, and you simply place a ContentControl on your view with the same name as the property:
<ContentControl x:Name="ActiveItem" />
You can use the ActivateItem() method to change the current active item.
Caliburn.Micro also has a host of other features, such as being able to place a Button control with x:Name="Save" on your view, and your Save method on your view model will automatically be invoked when the button is clicked.
Every VM is a DependencyObject. Every property is a
DependencyProperty.
why? a viewmodel should be a simple class with INotifyPropertyChanged and the Properties should be simple properties.
and if you want your different viewmodel be rendered in a different way - you should use DataTemplate.
<Window>
<Window.Resources>
<DataTemplate DataType="{x:Type local:MyViewModelA}>
<MyViewA/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:MyViewModelB}>
<MyViewB/>
</DataTemplate>
</Windows.Resources>
<Grid>
<ContentControl Content="{Binding MyActualVM}"/>
</Grid>
</Window>
EDIT: btw you always bind to the last Property: FooViewModel.BarViewModel.View --> so the INotifyPropertyChanged (if raised) just work for the .View
EDIT2: another approach could be to get the BindingExpression of your content control and call.
System.Windows.Data.BindingExpression expr = //get it from your contentcontrol
expr.UpdateTarget();
EDIT3: and a simple mvvm way - just use INotifyPropertyChanged
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.MyFooVM = new FooVM();
this.MyFooVM.MyBarVM = new BarVM(){View = "erster"};
this.DataContext = this;
}
public FooVM MyFooVM { get; set; }
private void Button_Click(object sender, RoutedEventArgs e)
{
this.MyFooVM.MyBarVM = new BarVM(){View = "zweiter"};
}
}
public class INPC : INotifyPropertyChanged
{
#region Implementation of INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropChanged(string property)
{
var handler = PropertyChanged;
if(handler != null)
handler(this, new PropertyChangedEventArgs(property));
}
#endregion
}
public class FooVM:INPC
{
private BarVM _myBarVm;
public BarVM MyBarVM
{
get { return _myBarVm; }
set { _myBarVm = value;OnPropChanged("MyBarVM"); }
}
}
public class BarVM : INPC
{
private string _view;
public string View
{
get { return _view; }
set { _view = value;OnPropChanged("View"); }
}
}
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 thought 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.