Input and Output from User Control in WPF with MVVM pattern

09 May 2020

Category: Mini

Hi everyone,
last week, I was struggling to fully understand data-binding and sending data to user control and receiving data from it. In the end, I came up with this simple project where there is everything implemented in a very minimalistic way. If you run the project, you see two visually identical parts; the upper one is on the form and the lower one is user control. Data are bound between those two, so when you write one text in the Name field and other text in the Surname field, they are being concatenated in the user control, and this concatenated text is displayed back in the field on the form.
I hope this could help some other fellow programmers to wrap their heads around this topic and use it in a meaningful way in bigger projects. I’m posting the code here, but if you want to see the full project, the link to the repo is below.

Main Window:

 <Window x:Class="OutputFromUserControl.View.OutputFromUserControlWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:uc="clr-namespace:OutputFromUserControl.View.Controls"
        xmlns:vm="clr-namespace:OutputFromUserControl.ViewModel"
        mc:Ignorable="d"
        Title="Output From User Control" Height="350" Width="600">

    <Window.DataContext>
        <vm:MainVM x:Name="MainVM"/>
    </Window.DataContext>
    
    <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
        <Label Content="Form elements:"/>
        <Border CornerRadius="5" BorderBrush="Blue" BorderThickness="1">
            <Grid HorizontalAlignment="Left" >
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="auto"/>
                    <ColumnDefinition Width="auto"/>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="auto"/>
                    <RowDefinition Height="auto"/>
                    <RowDefinition Height="auto"/>
                </Grid.RowDefinitions>

                <Label Content="Name Input: " Grid.Row="0" Grid.Column="0"/>
                <TextBox Grid.Row="0" Grid.Column="1" 
                         Name="FirstnameInput"
                     Text="{Binding NameInput, UpdateSourceTrigger=PropertyChanged}"
                     Width="200"
                     />
                <Label Content="Surname Input: " Grid.Row="1" Grid.Column="0"/>
                <TextBox Grid.Row="1" Grid.Column="1" 
                         Name="SurnameInput"
                     Text="{Binding SurnameInput, UpdateSourceTrigger=PropertyChanged}"
                     Width="200"
                     />
                <Label Content="Name Output from Control: " Grid.Row="2" Grid.Column="0"/>
                <TextBlock Grid.Row="2" Grid.Column="1" 
                           Width="200"
                     >
                    <TextBlock.Text>
                        <MultiBinding StringFormat="{}{0} {1}">
                            <Binding ElementName="FirstnameInput" Path="Text"></Binding>
                            <Binding ElementName="SurnameInput" Path="Text"></Binding>
                        </MultiBinding>
                    </TextBlock.Text>
                </TextBlock>
            </Grid>
        </Border>
        <Label Content="User Control:" Margin="0,10,0,0"/>
        <Border CornerRadius="5" BorderBrush="Red" BorderThickness="1">
            <uc:NameConcatControl x:Name="NameUC"
                                  NameInput="{Binding NameInput}" 
                                  SurnameInput="{Binding SurnameInput}"
                                  NameOutput="{Binding FullName, Mode=TwoWay}"
                                  />
        </Border>
    </StackPanel>
</Window>

MainVM:

 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
 using System.Text;
 
 namespace OutputFromUserControl.ViewModel
 {
     public class MainVM : INotifyPropertyChanged
     {
         private string nameInput;
 
         public string NameInput {
             get { return nameInput; }
             set 
             {
                 nameInput = value;
                 OnPropertyChanged(nameof(NameInput));
             }
         }
 
         private string surnameInput;
 
         public string SurnameInput {
             get { return surnameInput; }
             set {
                 surnameInput = value;
                 OnPropertyChanged(nameof(SurnameInput));
             }
         }
 
         private string fullName;
 
         public string FullName {
             get { return fullName; }
             set {
                 fullName = value;
                 OnPropertyChanged(nameof(FullName));
             }
         }
 
         public event PropertyChangedEventHandler PropertyChanged;
 
         private void OnPropertyChanged(string propertyName)
         {
             if (PropertyChanged != null)
                 PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
         }
     }
 }

Control xaml:

 <UserControl x:Class="OutputFromUserControl.View.Controls.NameConcatControl"
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
              xmlns:local="clr-namespace:OutputFromUserControl.View.Controls"
              mc:Ignorable="d" 
              d:DesignHeight="450" d:DesignWidth="800">
     <Grid>
         <Grid.ColumnDefinitions>
             <ColumnDefinition Width="auto"/>
             <ColumnDefinition Width="auto"/>
         </Grid.ColumnDefinitions>
         <Grid.RowDefinitions>
             <RowDefinition Height="auto"/>
             <RowDefinition Height="auto"/>
             <RowDefinition Height="auto"/>
         </Grid.RowDefinitions>
 
         <Label Content="Name Input: " Grid.Row="0" Grid.Column="0"/>
         <TextBlock Grid.Row="0" Grid.Column="1" 
                    Text="{Binding NameInput}"
                    x:Name="NameInputTextBlock"
                    />
         <Label Content="Surname Input: " Grid.Row="1" Grid.Column="0"/>
         <TextBlock Grid.Row="1" Grid.Column="1" 
                    Text="{Binding SurnameInput}"
                    x:Name="SurnameInputTextBlock"
                    />
         <Label Content="Name Output: " Grid.Row="2" Grid.Column="0"/>
         <TextBlock Grid.Row="2" Grid.Column="1" 
                    Text="{Binding NameOutput}"
                    x:Name="OutputNameTextBlock"
                    />
     </Grid>
 </UserControl>

User control .cs:

 using System.Windows;
 using System.Windows.Controls;
 
 namespace OutputFromUserControl.View.Controls
 {
     /// <summary>
     /// Interaction logic for NameConcatControl.xaml
     /// </summary>
     public partial class NameConcatControl : UserControl
     {
         public string NameInput {
             get { return (string)GetValue(NameInputProperty); }
             set { SetValue(NameInputProperty, value); }
         }
 
         public static string defaultNameInput = "NameInput";
         public static readonly DependencyProperty NameInputProperty =
             DependencyProperty.Register("NameInput", typeof(string), typeof(NameConcatControl), new PropertyMetadata(defaultNameInput, SetNameOutput));
 
 
         public string SurnameInput {
             get { return (string)GetValue(SurnameInputProperty); }
             set { SetValue(SurnameInputProperty, value); }
         }
 
         public static string defaultSurnameInput = "Surname Input";
         public static readonly DependencyProperty SurnameInputProperty =
             DependencyProperty.Register("SurnameInput", typeof(string), typeof(NameConcatControl), new PropertyMetadata(defaultSurnameInput, SetNameOutput));
 
 
         public string NameOutput {
             get { return (string)GetValue(NameOutputProperty); }
             set { SetValue(NameOutputProperty, value); }
         }
 
         public static string defaultNameOutput = "Name Output";
         public static readonly DependencyProperty NameOutputProperty =
             DependencyProperty.Register("NameOutput", typeof(string), typeof(NameConcatControl), new PropertyMetadata(defaultNameOutput));
 
 
         private static void SetNameOutput(DependencyObject d, DependencyPropertyChangedEventArgs e)
         {
             NameConcatControl control = (NameConcatControl)d;
 
             string nameInput = "";
             string surnameInput = "";
 
             if(e.Property.Name == "NameInput")
             {
                 string newValue = (string)e.NewValue;
                 nameInput = string.IsNullOrEmpty(newValue) ? "" : newValue;
             }
             else
             {
                 nameInput = string.IsNullOrEmpty(control.NameInputTextBlock.Text)
                 ? ""
                 : control.NameInputTextBlock.Text;
             }
 
             if(e.Property.Name == "SurnameInput")
             {
                 string newValue = (string)e.NewValue;
                 surnameInput = string.IsNullOrEmpty(newValue) ? "" : newValue;
             }
             else
             {
                 surnameInput = string.IsNullOrEmpty(control.SurnameInputTextBlock.Text)
                 ? ""
                 : control.SurnameInputTextBlock.Text;
             }
 
             string fullName = $"{nameInput} {surnameInput}";
 
             control.OutputNameTextBlock.Text = fullName;
             control.NameOutput = fullName;
         }
 
         public NameConcatControl()
         {
             InitializeComponent();
         }
     }
 }

Cheers!

Petr