When creating a WPF application, laying out design is easy. Once the layout is populated with data, everything can change. Fortunately there is a simple approach to add data to aid in design that will not have any relevance at runtime.
In the Model View ViewModel design pattern, the ViewModel contains all the properties that drive the view. These properties must raise the PropertyChanged event in order for the updates to be reflected on the User Interface. A basic ViewModel appears as follows.
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
namespace TestApp
{
public class MainUserControlViewModel : DependencyObject,
INotifyPropertyChanged, INotifyDataErrorInfo
{
/// <summary>
/// Parameterless constructor that does something with a service maybe
/// </summary>
public MainUserControlViewModel(){
// Something that throws if service not running
}
private bool _isReady = true;
private string _error = "None";
private string _message;
/// <summary>
/// A dictionary to track errors
/// </summary>
protected Dictionary<string, IEnumerable<string>> ValidationErrors =
new Dictionary<string, IEnumerable<string>>();
#region Implementation of INotifyPropertyChanged
/// <summary>
/// Occurs when a property value changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Implementation of INotifyDataErrorInfo
/// <summary>
/// The event for a change in error state
/// </summary>
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
/// <summary>
/// Gets the errors for a given property.
/// </summary>
/// <param name="propertyName">The property possibly containing errors</param>
/// <returns>A list of the errors</returns>
public IEnumerable GetErrors(string propertyName)
{
if (string.IsNullOrEmpty(propertyName) || !ValidationErrors.ContainsKey(propertyName))
return null;
return ValidationErrors[propertyName];
}
/// <summary>
/// Checks if there are any errors on the View Model
/// </summary>
public bool HasErrors => ValidationErrors.Count > 0 ?
true :
(_error!=null && _error.ToLowerInvariant() != "None".ToLowerInvariant());
#endregion
/// <summary>
/// Gets or sets the error.
/// </summary>
/// <value>
/// The error.
/// </value>
public string Error
{
get { return _error; }
set { SetField(ref _error, value); }
}
/// <summary>
/// Whether or not the View Model is ready
/// </summary>
/// <value>
/// The view model should set this to false when running
/// </value>
public bool IsReady
{
get { return _isReady; }
set { SetField(ref _isReady, value); }
}
/// <summary>
/// Gets or sets the message.
/// </summary>
/// <value>
/// The message.
/// </value>
public string Message
{
get { return _message; }
set { SetField(ref _message, value); }
}
/// <summary>
/// Raises the property changed.
/// </summary>
/// <param name="propertyName">Name of the property.</param>
public virtual void RaisePropertyChanged(string propertyName = null)
{
OnPropertyChanged(propertyName);
}
/// <summary>
/// Called when [property changed].
/// </summary>
/// <param name="propertyName">Name of the property.</param>
protected virtual void OnPropertyChanged(string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <summary>
/// Sets the field.
/// </summary>
/// <typeparam name="T">The generic type</typeparam>
/// <param name="field">The field.</param>
/// <param name="value">The value.</param>
/// <param name="propertyName">Name of the property.</param>
/// <returns>True if the field has changed value</returns>
protected virtual bool SetField<T>(ref T field, T value,
[CallerMemberName] string propertyName = null)
{
if (!EqualityComparer<T>.Default.Equals(field, value))
{
field = value;
OnPropertyChanged(propertyName);
return true;
}
return false;
}
/// <summary>
/// Manually raise the Errors changed event
/// </summary>
/// <param name="propertyName">The name of the property</param>
protected void RaiseErrorsChanged(string propertyName)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
}
}
In this example, the ViewModel relies on a service, so its parameter-less construction will throw an exception. This will prohibit it from presenting any content to the View during design. Fortunately, a design context can be identified in the XAML that will ensure it is not called from the design development environment.
<UserControl x:Class="TestApp.MainUserControl"
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:TestApp"
mc:Ignorable="d"
d:DesignHeight="48"
d:DesignWidth="60"
d:DataContext="{d:DesignInstance {x:Type local:MainUserControlDesignViewModel}, IsDesignTimeCreatable=True}">
<UserControl.DataContext>
<local:MainUserControlViewModel />
</UserControl.DataContext>
<StackPanel IsEnabled="{Binding IsReady, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}">
<TextBox Text="{Binding Message, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></TextBox>
<Label Content="{Binding Error, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></Label>
</StackPanel>
</UserControl>
Once this designation is made, the class must exist.
namespace TestApp
{
internal class MainUserControlDesignViewModel
{
public string Message { get; set; } = "Test";
public string Error { get; set; } = "None";
public bool IsReady { get; set; } = true;
}
}
{ get; set; }
is required to make Message, Error, and IsReady properties, and not fields.
The class will not provide any design context for fields.
Regardless of the reason (even if the full ViewModel doesn't throw an exception), design data is simply good practice. Using XAML, it is easy to create these properties and set default values that will only appear during the design phase.