When we need to arrange a number of existing controls in a particular way, we typically use a UserControl object. This is why we normally use this type of control to build our Views. However, when we need to build a reusable control, such as an address control, we tend to separate these from our Views, by declaring them in a Controls folder and namespace within our Views project.
When declaring these reusable controls, it is customary to define Dependency Properties in the code behind and as long as there is no business-related functionality in the control, it is also OK to use the code behind to handle events. If the control is business-related, then we can use a View Model as we do with normal Views. Let's take a look at an example of an address control:
<UserControl x:Class= "CompanyName.ApplicationName.Views.Controls.AddressControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Controls= "clr-namespace:CompanyName.ApplicationName.Views.Controls"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" SharedSizeGroup="Label" /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <TextBlock Text="House/Street" /> <TextBox Grid.Column="1" Text="{Binding Address.HouseAndStreet, RelativeSource={RelativeSource AncestorType={x:Type Controls:AddressControl}}}" /> <TextBlock Grid.Row="1" Text="Town" /> <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Address.Town, RelativeSource={RelativeSource AncestorType={x:Type Controls:AddressControl}}}" /> <TextBlock Grid.Row="2" Text="City" /> <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Address.City, RelativeSource={RelativeSource AncestorType={x:Type Controls:AddressControl}}}" /> <TextBlock Grid.Row="3" Text="Post Code" /> <TextBox Grid.Row="3" Grid.Column="1" Text="{Binding Address.PostCode, RelativeSource={RelativeSource AncestorType={x:Type Controls:AddressControl}}}" /> <TextBlock Grid.Row="4" Text="Country" /> <TextBox Grid.Row="4" Grid.Column="1" Text="{Binding Address.Country, RelativeSource={RelativeSource AncestorType={x:Type Controls:AddressControl}}}" /> </Grid> </UserControl>
In this example, we declare this class within the Controls namespace and set up a XAML namespace prefix for it. We then see the Grid panel that is used to layout the address controls and notice that the SharedSizeGroup property is set on the ColumnDefinition element that defines the label column. This will enable the column sizes within this control to be shared with externally declared controls.
We then see all of the TextBlock and TextBox controls that are data bound to the control's address fields. There's not much to note here except that the data bound properties are all accessed through a RelativeSource binding to an Address Dependency Property that is declared in the code behind file of the AddressControl.
Remember that it's fine to do this when using MVVM as long as we are not encapsulating any business rules here. Our control merely enables the users to input or add address information, which will be used by various Views and View Models. Let's see this property now:
using System.Windows; using System.Windows.Controls; using CompanyName.ApplicationName.DataModels; namespace CompanyName.ApplicationName.Views.Controls { public partial class AddressControl : UserControl { public AddressControl() { InitializeComponent(); } public static readonly DependencyProperty AddressProperty = DependencyProperty.Register(nameof(Address), typeof(Address), typeof(AddressControl), new PropertyMetadata(new Address())); public Address Address { get { return (Address)GetValue(AddressProperty); } set { SetValue(AddressProperty, value); } } } }
This is a very simple control with just one Dependency Property. We can see that the Address property is of type Address, so let's have a quick look at that class next:
namespace CompanyName.ApplicationName.DataModels { public class Address : BaseDataModel { private string houseAndStreet, town, city, postCode, country; public string HouseAndStreet { get { return houseAndStreet; } set { if (houseAndStreet != value) { houseAndStreet = value; NotifyPropertyChanged(); } } } public string Town { get { return town; } set { if (town != value) { town = value; NotifyPropertyChanged(); } } } public string City { get { return city; } set { if (city != value) { city = value; NotifyPropertyChanged(); } } } public string PostCode { get { return postCode; } set { if (postCode != value) { postCode = value; NotifyPropertyChanged(); } } } public string Country { get { return country; } set { if (country != value) { country = value; NotifyPropertyChanged(); } } } public override string ToString() { return $"{HouseAndStreet}, {Town}, {City}, {PostCode}, {Country}"; } } }
Again, we have a very simple class that is primarily made up from the address related properties. Note the use of the String Interpolation in the overridden ToString method to output a useful display of the class contents. Now we've seen the control, let's take a look at how we can use it in our application. We can edit a View that we saw earlier, so let's see the updated UserView XAML now:
<Grid TextElement.FontSize="14" Grid.IsSharedSizeScope="True" Margin="10"> <Grid.Resources> <Style TargetType="{x:Type TextBlock}"> <Setter Property="HorizontalAlignment" Value="Right" /> <Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="Margin" Value="0,0,5,5" /> </Style> <Style TargetType="{x:Type TextBox}"> <Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="Margin" Value="0,0,0,5" /> </Style> </Grid.Resources> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" SharedSizeGroup="Label" /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <TextBlock Text="Name" /> <TextBox Grid.Column="1" Text="{Binding User.Name}" /> <TextBlock Grid.Row="1" Text="Age" /> <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding User.Age}" /> <Controls:AddressControl Grid.Row="2" Grid.ColumnSpan="2" Address="{Binding User.Address}" /> </Grid>
In this example, we can see the use of the Grid.IsSharedSizeScope property on the outermost Grid panel. Remember that the SharedSizeGroup property was set in the AddressControl XAML, although without this setting on the outer Grid, it does nothing by itself.
Looking at the outer panel's column definitions, we can see that we have also set the SharedSizeGroup property to the same value of Label on the left column so that the two panels' columns will be aligned.
We can skip over the two styles that are declared in the panel's Resources section as in a proper application, these would most likely reside in the application resources file. In the remainder of the View, we simply have a couple of rows of user properties and then AddressControl.
This code assumes that we have declared an Address property of type Address in our User class and populated it with suitable values in the UserViewModel class. Note how we data bind the Address property of the User class to the Address property of the control, rather than setting the DataContext property. As the control's internal controls are data bound using RelativeSource bindings, which specify their own binding source, they do not require any DataContext to be set. In fact, doing so in this example would stop it from working.