Problem
I’m simulating my real-world application by using a really simple demo app here, but it has the same concept. The question is MVVM Light Toolkit
specific, as the purpose of my project is to learn the workings of the toolkit.
Let’s say I have a Main
form, inside which I have a UserControl
. The Main
has a stand-alone TextBox
, and the user control itself contains a single TextBox
. It would look something like this:
The goal is this: if the user types on the Main
form TextBox
, I want to see the text displayed on the UserControl
‘s TextBox
, and wise-versa, in real time. I’m using the TextChanged
event.
UserControl XAML
When all the unnecessary clutter is removed my UserControl
XAML looks like this:
<UserControl ...
d_DesignHeight="80" d_DesignWidth="280">
<Grid Background="LightSteelBlue">
<TextBox Name="TxtDisplay" Background="PaleGreen" Grid.Row="1"
Width="250" Height="32"
TextChanged="TxtDisplay_TextChanged"
Text="{Binding Display, RelativeSource={RelativeSource FindAncestor, AncestorType=UserControl}}">
</TextBox>
</Grid>
</UserControl>
UserControl Code Behind
I have a RoutedEvent
so I can bubble up the TextChanged
event, and a DependancyProperty
to send the content of the TextBox
to the Main
.
public partial class TextDisplay : UserControl
{
public TextDisplay()
{
InitializeComponent();
}
public event RoutedEventHandler RoutedTextChanged;
public static readonly DependencyProperty DisplayProperty = DependencyProperty.Register("Display", typeof(string), typeof(TextDisplay));
public string Display
{
get { return (string)GetValue(DisplayProperty); }
set
{
SetValue(DisplayProperty, value);
}
}
private void TxtDisplay_TextChanged(object sender, TextChangedEventArgs e)
{
Display = TxtDisplay.Text;
RoutedTextChanged?.Invoke(this, new RoutedEventArgs());
}
}
Main Window XAML
DataContext
is bound to the MainViewModel
.
<Window ...
DataContext="{Binding Main, Source={StaticResource Locator}}">
<Grid x_Name="LayoutRoot">
<TextBox Grid.Row="0" Name="TxtInput" Width="200" Height="32"
Text="{Binding MainDisplay, UpdateSourceTrigger=PropertyChanged}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="TextChanged">
<i:InvokeCommandAction Command="{Binding MainDisplayTextChangedCommand}"
CommandParameter="{Binding ElementName=TxtInput, Path=Text}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
<uc:TextDisplay Grid.Row="1" Width="300" Height="80"
Display="{Binding UCDisplayText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="RoutedTextChanged">
<i:InvokeCommandAction Command="{Binding UCDisplayTextChangedCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</uc:TextDisplay>
</Grid>
</Window>
MainViewModel
public class MainViewModel : ViewModelBase
{
public const string MainDisplayPropertyName = "MainDisplay";
private string _mainDisplay = string.Empty;
public string MainDisplay
{
get { return _mainDisplay; }
set
{
if (_mainDisplay == value) { return; }
_mainDisplay = value;
RaisePropertyChanged(() => MainDisplay);
}
}
public const string UCDisplayTextPropertyName = "UCDisplayText";
private string _ucDisplayText = string.Empty;
public string UCDisplayText
{
get { return _ucDisplayText; }
set
{
if (_ucDisplayText == value) { return; }
_ucDisplayText = value;
RaisePropertyChanged(() => UCDisplayText);
}
}
public MainDisplayTextChanged MainDisplayTextChangedCommand { get; private set; }
public UCDisplayTextChanged UCDisplayTextChangedCommand { get; private set; }
public MainViewModel(IDataService dataService)
{
MainDisplayTextChangedCommand = new MainDisplayTextChanged(this);
UCDisplayTextChangedCommand = new UCDisplayTextChanged(this);
}
public void MainTextChanged()
{
UCDisplayText = MainDisplay;
}
public void UCTextChanged()
{
MainDisplay = UCDisplayText;
}
}
TextChanged Commands
The two TextChanged
command classes derive from ICommand
and is designed according to the pattern shown in the Official MVVM Toolkit Tutorial
on PluralSight.
public class MainDisplayTextChanged : ICommand
{
protected readonly MainViewModel _owner;
public MainDisplayTextChanged(MainViewModel owner)
{
_owner = owner;
_owner.PropertyChanged += (s, e) =>
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
};
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_owner.MainTextChanged();
}
}
public class UCDisplayTextChanged : ICommand
{
protected readonly MainViewModel _owner;
public UCDisplayTextChanged(MainViewModel owner)
{
_owner = owner;
_owner.PropertyChanged += (s, e) =>
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
};
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_owner.UCTextChanged();
}
}
QUESTIONS
Now this works, but I have this feeling that this might not quite be the right way to go about this. Particularly:
- In
TextDisplay.xaml.cs
code’sTextChanged
event, if I don’t doDisplay = TxtDisplay.Text;
then theDataBinding
alone doesn’t update theDependancyProperty
value. What am I doing wrong? - Can I avoid using
DependancyProperty
altogether? I know I can bind to a property of theDataContext
and have changes reflected instantly, but I’m looking for a decoupled solution where the `UserControl can stand on it’s own. - Is there a better design for the two
ICommand
derivative classes? - Any other comments/suggestions are also welcome.
EDIT
Download the entire project here.
Thank you!
Solution
Simplifications
What you want to do can be achieved with data-binding alone, you don’t need all those commands.
-
Since both text-boxes should display the same content, we only need one string property in
MainViewModel
:private string _mainDisplay = ""; public string MainDisplay { get => _mainDisplay; set { _mainDisplay = value; RaisePropertyChanged(); } }
Note the lack of
() => MainDisplay
: becauseRaisePropertyChanges
has an overload with a[CallerMemberName]
parameter, the compiler will automatically insert the name of the property for us. Alternately, you can usenameof(MainDisplay)
. -
Your user-control should bind to this property, just like the text-box in
MainWindow.xaml
:Display="{Binding MainDisplay, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
. -
Now you can remove those command classes and anything related to them, including the
<i:Interaction.Trigger>
parts inMainWindow.xaml
and theMainTextChanged
andUCTextChanged
methods inMainViewModel
. -
There’s just one little detail left: we want changes in the user-control to be visible elsewhere immediately, but the textbox in
TextDisplay.xaml
doesn’t have itsUpdateSourceTrigger
set toPropertyChanged
. Fix that, and everything should work as expected without thatTextChanged
callback.
Answers
Regarding dependency properties, as far as I know WPF’s data binding system requires either the source or target (or both) to be a dependency property, so if you don’t want to set the DataContext
of your user-control, then yes, you’ll need to use dependency properties.
Which approach to take depends on the purpose of your user-control. If it’s meant to display a certain item, then just set the DataContext
, so an ItemUserControl
always binds to a matching ItemViewModel
. On the other hand, if a user-control is a reusable UI control (such as a fancy date-picker) then it makes sense to use dependency properties.
Regarding commands, the MVVM Light library comes with very two useful RelayCommand
classes. In my experience you rarely need anything else. I mostly use commands for high-level actions (open project, save file, copy/paste, etc.), and they’re usually tied to menu items or buttons via data-binding: <Button Command="{Binding OpenFileCommand}" />
.
Other notes
- Properties that are only set once (in the constructor) don’t need a
private set
,{ get; }
is sufficient nowadays. - Dependency properties can also be initialized with a
PropertyMetadata
object. This allows you to specify a default value and an on-changed callback. But you can also use aFrameworkPropertyMedadata
, which offers additional settings. One useful setting in particular isFrameworkPropertyMetadataOptions.BindsTwoWayByDefault
. - MVVM Light provides a
Set<T>
method that allows you to simplify your property setters toset => Set(ref _mainDisplay, value);
. This will set the backing field and raise aPropertyChangedEvent
.