1
\$\begingroup\$

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:

enter image description here

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:

  1. In TextDisplay.xaml.cs code's TextChanged event, if I don't do Display = TxtDisplay.Text; then the DataBinding alone doesn't update the DependancyProperty value. What am I doing wrong?
  2. Can I avoid using DependancyProperty altogether? I know I can bind to a property of the DataContext and have changes reflected instantly, but I'm looking for a decoupled solution where the `UserControl can stand on it's own.
  3. Is there a better design for the two ICommand derivative classes?
  4. Any other comments/suggestions are also welcome.

EDIT

Download the entire project here.

Thank you!

\$\endgroup\$
1
\$\begingroup\$

Simplifications

What you want to do can be achieved with data-binding alone, you don't need all those commands.

  1. 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: because RaisePropertyChanges has an overload with a [CallerMemberName] parameter, the compiler will automatically insert the name of the property for us. Alternately, you can use nameof(MainDisplay).

  2. Your user-control should bind to this property, just like the text-box in MainWindow.xaml: Display="{Binding MainDisplay, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}".

  3. Now you can remove those command classes and anything related to them, including the <i:Interaction.Trigger> parts in MainWindow.xaml and the MainTextChanged and UCTextChanged methods in MainViewModel.

  4. 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 its UpdateSourceTrigger set to PropertyChanged. Fix that, and everything should work as expected without that TextChanged 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 a FrameworkPropertyMedadata, which offers additional settings. One useful setting in particular is FrameworkPropertyMetadataOptions.BindsTwoWayByDefault.
  • MVVM Light provides a Set<T> method that allows you to simplify your property setters to set => Set(ref _mainDisplay, value);. This will set the backing field and raise a PropertyChangedEvent.
\$\endgroup\$
  • \$\begingroup\$ That's very detailed, thanks! Regarding the Simplification, that's elegant, I will definitely use that. Re. Dependency Properties and UserControl, I'm mostly looking to develop a few reusable UserControls to use in my work, so it's crucial that they are decoupled. I will explore more about the Dependency Property option. Do you have any suggestions about the RelayCommand, a place to learn, a tutorial? There's a ton of them out there, so I welcome any personal favorite kinda suggestions. \$\endgroup\$ – Sach Sep 26 '18 at 16:53
  • \$\begingroup\$ Those relay command classes are quite easy to use. In my button example, the view-model would contain a public ICommand OpenFileCommand { get; } property, and initialize that in the constructor to OpenFileCommand = new RelayCommand(OpenFile);, where OpenFile is a method in your view-model class. Pressing the button will now invoke the OpenFile method. \$\endgroup\$ – Pieter Witvoet Sep 27 '18 at 9:05
  • \$\begingroup\$ Also note that commands can be 'disabled', which also disables any button or menu item that binds to them. That's what the RelayCommand(Action execute, Func<bool> canExecute) constructor and the RaiseCanExecuteChanged method are for. \$\endgroup\$ – Pieter Witvoet Sep 27 '18 at 9:09
  • \$\begingroup\$ WOW that's neat. Thanks, you've been really helpful, wish I could upvote more than once. \$\endgroup\$ – Sach Sep 27 '18 at 16:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.