WPF (.NET) Learning Notes
1. MVVM pattern and toolkit
The MVVM pattern [1] helps cleanly separate an application’s business and presentation logic from its user interface (UI).
-
Maintaining a clean separation between application logic and the UI helps address numerous development issues and makes an application easier to test, maintain, and evolve.
-
It can also significantly improve code re-use opportunities and allows developers and UI designers to collaborate more easily when developing their respective parts of an app.
1.1. MVVM pattern
There are three core components in the MVVM pattern: the model, the view, and the view model.
Using the MVVM pattern, the UI of the app and the underlying presentation and business logic are separated into three separate classes: the view, which encapsulates the UI and UI logic; the view model, which encapsulates presentation logic and state; and the model, which encapsulates the app’s business logic and data.
1.1.1. View
The view is responsible for defining the structure, layout, and appearance of what the user sees on screen.
-
Ideally, each view is defined in XAML, with a limited code-behind that does not contain business logic.
However, in some cases, the code-behind might contain UI logic that implements visual behavior that is difficult to express in XAML, such as animations.
-
Enable and disable UI elements by binding to view model properties, rather than enabling and disabling them in code-behind.
Ensure that the view models are responsible for defining logical state changes that affect some aspects of the view’s display, such as whether a command is available, or an indication that an operation is pending.
-
There are several options for executing code on the view model in response to interactions on the view, such as a button click or item selection.
-
If a control supports commands, the control’s
Command
property can be data-bound to anICommand
property on the view model.When the control’s command is invoked, the code in the view model will be executed.
-
In addition to commands, behaviors can be attached to an object in the view and can listen for either a command to be invoked or the event to be raised.
In response, the behavior can then invoke an ICommand on the view model or a method on the view model.
-
1.1.2. ViewModel
The view model implements properties and commands to which the view can data bind to, and notifies the view of any state changes through change notification events. The properties and commands that the view model provides define the functionality to be offered by the UI, but the view determines how that functionality is to be displayed.
-
Multi-platform apps should keep the UI thread unblocked to improve the user’s perception of performance.
Therefore, in the view model, use asynchronous methods for I/O operations and raise events to asynchronously notify views of property changes.
-
The view model is also responsible for coordinating the view’s interactions with one or many model classes that are required.
The view model might choose to expose model classes directly to the view so that controls in the view can data bind directly to them. In this case, the model classes will need to be designed to support data binding and change notification events.
-
Each view model provides data from a model in a form that the view can easily consume.
-
Placing the data conversion in the view model is a good idea because it provides properties that the view can bind to. For example, the view model might combine the values of two properties to make it easier to display by the view.
-
It’s also possible to use converters as a separate data conversion layer that sits between the view model and the view. This can be necessary, for example, when data requires special formatting that the view model doesn’t provide.
-
-
In order for the view model to participate in two-way data binding with the view, its properties must raise the
PropertyChanged
event.-
View models satisfy this requirement by implementing the
INotifyPropertyChanged
interface, and raising thePropertyChanged
event when a property is changed. -
For collections, the view-friendly
ObservableCollection<T>
is provided.This collection implements collection changed notification, relieving the developer from having to implement the
INotifyCollectionChanged
interface on collections.
-
1.1.3. Model
Model classes are non-visual classes that encapsulate the app’s data.
-
Model classes can be thought of as representing the app’s domain model that includes a data model along with business and validation logic.
-
Examples of model objects include data transfer objects (DTOs), Plain Old CLR Objects (POCOs), and generated entity and proxy objects.
-
Model classes are typically used in conjunction with services or repositories that encapsulate data access and caching.
1.1.4. Connecting view models to views
View models can be connected to views by using the data-binding capabilities.
Creating a view model declaratively
The simplest approach is for the view to declaratively instantiate its corresponding view model in XAML. When the view is constructed, the corresponding view model object will also be constructed.
<Grid.DataContext>
<vm:MainViewModel />
</Grid.DataContext>
Though the declarative construction and assignment of the view model by the view has the advantage that it’s simple, but has the disadvantage that it requires a default (parameter-less) constructor in the view model.
Creating a view model programmatically
A view can have code in the code-behind file, resulting in the view-model being assigned to its DataContext
property.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
}
The programmatic construction and assignment of the view model within the view’s code-behind has the advantage that it’s simple.
-
However, the main disadvantage of this approach is that the view needs to provide the view model with any required dependencies.
-
Using a dependency injection container can help to maintain loose coupling between the view and view model.
1.1.5. Updating views in response to changes in the underlying view model or model
All view model and model classes that are accessible to a view should implement the INotifyPropertyChanged
interface. Implementing this interface in a view model or model class allows the class to provide change notifications to any data-bound controls in the view when the underlying property value changes.
-
Always raising a
PropertyChanged
event if a public property’s value changes. -
Always raising a
PropertyChanged
event for any calculated properties whose values are used by other properties in the view model or model. -
Always raising the
PropertyChanged
event at the end of the method that makes a property change, or when the object is known to be in a safe state. -
Never raising a
PropertyChanged
event if the property does not change. -
Never raising the
PropertyChanged
event during a view model’s constructor if you are initializing a property. -
Never raising more than one
PropertyChanged
event with the same property name argument within a single synchronous invocation of a public method of a class.
public sealed class MainViewModel : INotifyPropertyChanged
{
private string? _title;
public string? Title
{
get { return _title; }
set
{
if (_title != value)
{
_title = value;
OnPropertyChanged();
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
1.2. MVVM Toolkit
The CommunityToolkit.Mvvm
package (aka MVVM Toolkit, formerly named Microsoft.Toolkit.Mvvm
) is a modern, fast, and modular MVVM library. [2]
dotnet add package CommunityToolkit.Mvvm # --version 8.3.1
-
CommunityToolkit.Mvvm.ComponentModel
-
public abstract class ObservableObject : INotifyPropertyChanged, INotifyPropertyChanging
public sealed class MainViewModel : ObservableObject { private string? _title; public string? Title { get => _title; set => SetProperty(ref _title, value); } private TaskNotifier? myTask; public Task? MyTask { get => myTask; set => SetPropertyAndNotifyOnCompletion(ref myTask, value); } }
<StackPanel> <Label Content="{Binding MyTask.Status}" /> <TextBox Text="{Binding Title}" /> </StackPanel>
-
public abstract class ObservableRecipient : ObservableObject
public sealed class MainViewModel : ObservableRecipient, IRecipient<RequestMessage<int>> { public void Receive(RequestMessage<int> message) => throw new NotImplementedException(); }
public sealed class MainViewModel : ObservableRecipient { // For best results and to avoid memory leaks, it's recommended to use OnActivated to register // to messages, and to use OnDeactivated to do cleanup operations. protected override void OnActivated() => Messenger.Register<MainViewModel, RequestMessage<int>>(this, (r, m) => r.Receive(m)); // By default, OnDeactivated automatically unregisters the current instance from all registered messages. protected override void OnDeactivated() => base.OnDeactivated(); public void Receive(RequestMessage<int> message) => throw new NotImplementedException(); }
-
public abstract class ObservableValidator : ObservableObject, INotifyDataErrorInfo
public class RegistrationForm : ObservableValidator { private string name; [Required] [MinLength(2)] [MaxLength(100)] public string Name { get => name; // Here we are calling the SetProperty<T>(ref T, T, bool, string) method exposed // by ObservableValidator, and that additional bool parameter set to true indicates // that we also want to validate the property when its value is updated. set => SetProperty(ref name, value, true); } }
<TextBox Text="{Binding Name, ValidatesOnDataErrors=True}" />
-
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FullName))] // Notifying dependent properties [NotifyCanExecuteChangedFor(nameof(MyCommand))] // Notifying dependent commands [NotifyDataErrorInfo] // Requesting property validation [Required] [MinLength(2)] // Any other validation attributes too... [NotifyPropertyChangedRecipients] // Sending notification messages [PropertyChangedMessage<T>] [property: JsonPropertyName("name")] // Adding custom attributes private string? _name;
-
// only use in cases where the target types cannot just inherit from the equivalent // types (eg. from ObservableObject). [INotifyPropertyChanged] public partial class MyViewModel : SomeOtherType
-
-
CommunityToolkit.Mvvm.DependencyInjection
-
dotnet add package Microsoft.Extensions.DependencyInjection # --version 8.0.0
public partial class App : Application { public IServiceProvider Services { get; set; } public App() { Services = ConfigureServices(); } protected override void OnStartup(StartupEventArgs e) { var mainWindow = Services.GetRequiredService<MainWindow>(); mainWindow.Show(); } private ServiceProvider ConfigureServices() { var services = new ServiceCollection(); services.AddTransient<MainWindow>(); services.AddTransient<MainViewModel>(); return services.BuildServiceProvider(); } }
-
-
-
public sealed class MainViewModel : ObservableObject { public IRelayCommand OKCommand { get; } = new RelayCommand(() => { }, () => true); public IAsyncRelayCommand CancelCommand { get; } = new AsyncRelayCommand(() => Task.CompletedTask); }
-
[RelayCommand(CanExecute = nameof(CanGreetUser))] private void GreetUser(User? user) { Console.WriteLine($"Hello {user!.Name}!"); } private bool CanGreetUser(User? user) { return user is not null; } [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(GreetUserCommand))] private User? selectedUser;
// Call IAsyncRelayCommand.Cancel to signal that token. [RelayCommand(IncludeCancelCommand = true)] private async Task DoWorkAsync(CancellationToken token) { // Do some long running work... }
2. Data binding
Data binding is the process that establishes a connection between the app UI and the data it displays.
-
If the binding has the correct settings and the data provides the proper notifications, when the data changes its value, the elements that are bound to the data reflect changes automatically.
-
Data binding can also mean that if an outer representation of the data in an element changes, then the underlying data can be automatically updated to reflect the change.
Typically, each binding has four components:
-
A binding target object.
-
A target property.
-
A binding source.
-
A path to the value in the binding source to use.
For example, to bound the content of a TextBox
to the Employee.Name
property:
-
Target:
TextBox
-
Target property:
Text
-
Source object:
Employee
-
Source object value path:
Name
A binding contains all the information that can be shared across several binding expressions. A BindingExpression is an instance expression that cannot be shared and contains all the instance information of the Binding.
// Make a new source
var myDataObject = new MyData();
var myBinding = new Binding("ColorName") { Source = myDataObject };
// Bind the data source to the TextBox control's Text dependency property
myText.SetBinding(TextBlock.TextProperty, myBinding);
A binding source object can be treated either as a single object whose properties contain data or as a data collection of polymorphic objects that are often grouped together (such as the result of a query to a database).
Any collection that implements the IEnumerable
interface can be enumerated over. However, to set up dynamic bindings so that insertions or deletions in the collection update the UI automatically, the collection must implement the INotifyCollectionChanged interface.
WPF provides the ObservableCollection<T> class, which is a built-in implementation of a data collection that exposes the INotifyCollectionChanged
interface.
To fully support transferring data values from source objects to targets, each object in your collection that supports bindable properties must also implement the INotifyPropertyChanged
interface.
A collection view is a layer on top of a binding source collection that allows you to navigate and display the source collection based on sort, filter, and group queries, without having to change the underlying source collection itself.
-
A collection view also maintains a pointer to the current item in the collection.
If the source collection implements the
INotifyCollectionChanged
interface, the changes raised by theCollectionChanged
event are propagated to the views. -
Because views do not change the underlying source collections, each source collection can have multiple views associated with it.
Once ItemsControl is bound to a data collection, the data may need to be sorted, filtered, or grouped. To do that, use collection views, which are classes that implement the ICollectionView interface.
-
A collection view is a layer on top of a binding source collection that allows to navigate and display the source collection based on sort, filter, and group queries, without having to change the underlying source collection itself.
-
A collection view also maintains a pointer to the current item in the collection.
If the source collection implements the
INotifyCollectionChanged
interface, the changes raised by theCollectionChanged
event are propagated to the views. -
Because views do not change the underlying source collections, each source collection can have multiple views associated with it.
<Window.Resources>
<CollectionViewSource
Source="{Binding Source={x:Static Application.Current}, Path=AuctionItems}"
x:Key="listingDataView" />
</Window.Resources>
<ListBox Name="Master" Grid.Row="2" Grid.ColumnSpan="3" Margin="8"
ItemsSource="{Binding Source={StaticResource listingDataView}}" />
private void AddSortCheckBox_Checked(object sender, RoutedEventArgs e)
{
// Sort the items first by Category and then by StartDate
listingDataView.SortDescriptions.Add(new SortDescription("Category", ListSortDirection.Ascending));
listingDataView.SortDescriptions.Add(new SortDescription("StartDate", ListSortDirection.Ascending));
}
private void AddFilteringCheckBox_Checked(object sender, RoutedEventArgs e)
{
if (((CheckBox)sender).IsChecked == true)
listingDataView.Filter += ListingDataView_Filter;
else
listingDataView.Filter -= ListingDataView_Filter;
}
private void ListingDataView_Filter(object sender, FilterEventArgs e)
{
// Start with everything excluded
e.Accepted = false;
// Only inlcude items with a price less than 25
if (e.Item is AuctionItem product && product.CurrentPrice < 25)
e.Accepted = true;
}
// This groups the items in the view by the property "Category"
var groupDescription = new PropertyGroupDescription();
groupDescription.PropertyName = "Category";
listingDataView.GroupDescriptions.Add(groupDescription);
2.1. Data context
When data binding is declared on XAML elements, they resolve data binding by looking at their immediate DataContext property.
-
The data context is typically the binding source object for the binding source value path evaluation.
-
If the
DataContext
property for the object hosting the binding isn’t set, the parent element’sDataContext
property is checked, and so on, up until the root of the XAML object tree. -
In short, the data context used to resolve binding is inherited from the parent unless explicitly set on the object.
-
Bindings can be configured to resolve with a specific object, as opposed to using the data context for binding resolution.
-
When the
DataContext
property changes, all bindings that could be affected by the data context are reevaluated.
2.2. Data flow
-
OneWay binding causes changes to the source property to automatically update the target property, but changes to the target property are not propagated back to the source property, which is appropriate if the control being bound is implicitly read-only.
-
TwoWay binding causes changes to either the source property or the target property to automatically update the other, which is appropriate for editable forms or other fully interactive UI scenarios..
Most properties default to
OneWay
binding, but some dependency properties (typically properties of user-editable controls such as theTextBox.Text
andCheckBox.IsChecked
default toTwoWay
binding. A programmatic way to determine whether a dependency property binds one-way or two-way by default is to get the property metadata withDependencyProperty.GetMetadata
.if (TextBox.TextProperty.GetMetadata(typeof(TextBox)) is FrameworkPropertyMetadata meta) { Console.WriteLine($"{meta.BindsTwoWayByDefault}"); // True }
-
OneWayToSource is the reverse of OneWay binding; it updates the source property when the target property changes, which is appropriate if you only need to reevaluate the source value from the UI.
-
OneTime binding causes the source property to initialize the target property but doesn’t propagate subsequent changes which is appropriate if either a snapshot of the current state is appropriate or the data is truly static.
If the data context changes or the object in the data context changes, the change is not reflected in the target property.
To detect source changes (applicable to OneWay and TwoWay bindings), the source must implement a suitable property change notification mechanism such as INotifyPropertyChanged. |
Bindings that are TwoWay or OneWayToSource listen for changes in the target property and propagate them back to the source, known as updating the source.
The Binding.UpdateSourceTrigger property determines what triggers the update of the source.
If the UpdateSourceTrigger
value is UpdateSourceTrigger.PropertyChanged
, then the value pointed to by the right arrow of TwoWay
or the OneWayToSource
bindings is updated as soon as the target property changes.
However, if the UpdateSourceTrigger
value is LostFocus
, then that value only is updated with the new value when the target property loses focus.
If the UpdateSourceTrigger value of the binding is set to Explicit, the UpdateSource method must be called or the changes will not propagate back to the source.
var textBlock = new TextBlock();
var nameBindingObject = new Binding("Name");
nameBindingObject.UpdateSourceTrigger = UpdateSourceTrigger.Explicit;
// ...
textBlock.SetBinding(TextBlock.TextProperty, nameBindingObject);
var bindingExpression = textBlock.GetBindingExpression(TextBlock.TextProperty);
bindingExpression.UpdateSource();
2.3. Data conversion
[ValueConversion(typeof(Color), typeof(SolidColorBrush))]
public class ColorBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
Color color = (Color)value;
return new SolidColorBrush(color);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}
/// <summary>
/// Provides a way to apply custom logic to a binding.
/// </summary>
/// <remarks>Value converters are culture-aware. Both the Convert and ConvertBack methods have a culture parameter that indicates the cultural information.</remarks>
public interface IValueConverter
{
/// <summary>
/// The data binding engine calls this method when it propagates a value from the binding source to the binding target.
/// </summary>
/// <param name="value">The value produced by the binding source.</param>
/// <param name="targetType">The type of the binding target property.</param>
/// <param name="parameter">The converter parameter to use.</param>
/// <param name="culture">The culture to use in the converter.</param>
/// <returns>A converted value. If the method returns null, the valid null value is used.</returns>
/// <remarks>
/// A return value of <see cref="DependencyProperty.UnsetValue"/> indicates that the converter produced no value and that the binding uses the <see cref="BindingBase.FallbackValue"/>, if available, or the default value instead.
/// A return value of <see cref="Binding.DoNothing"/> indicates that the binding does not transfer the value or use the <see cref="BindingBase.FallbackValue"/> or default value.
/// </remarks>
object Convert(object value, Type targetType, object parameter, CultureInfo culture);
/// <summary>
/// The data binding engine calls this method when it propagates a value from the binding target to the binding source.
/// </summary>
/// <param name="value">The value that is produced by the binding target.</param>
/// <param name="targetType">The type to convert to.</param>
/// <param name="parameter">The converter parameter to use.</param>
/// <param name="culture">The culture to use in the converter.</param>
/// <returns>A converted value. If the method returns null, the valid null value is used.</returns>
/// <remarks>
/// A return value of <see cref="DependencyProperty.UnsetValue"/> indicates that the converter produced no value and that the binding uses the <see cref="BindingBase.FallbackValue"/>, if available, or the default value instead.
/// A return value of <see cref="Binding.DoNothing"/> indicates that the binding does not transfer the value or use the <see cref="BindingBase.FallbackValue"/> or default value.
/// </remarks>
object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture);
}
2.4. Data validation
WPF has two types of built-in ValidationRule objects to check whether the value of a property is valid.
-
A ExceptionValidationRule checks for exceptions thrown during the update of the binding source property.
An alternative syntax to setting the
ExceptionValidationRule
explicitly is to set the ValidatesOnExceptions property totrue
on aBinding
orMultiBinding
object. -
A DataErrorValidationRule object checks for errors that are raised by objects that implement the IDataErrorInfo or INotifyDataErrorInfo interface.
An alternative syntax to setting the
DataErrorValidationRule
explicitly is to set the ValidatesOnDataErrors property totrue
on aBinding
orMultiBinding
object. -
Custom validation rule objects can also be defined by deriving from the
ValidationRule
class and implementing theValidate
method.
One way to provide some feedback about the error on the app UI is to set the Validation.ErrorTemplate attached property to a custom ControlTemplate.
<ControlTemplate x:Key="validationTemplate">
<DockPanel>
<TextBlock Foreground="Red" FontSize="20">!</TextBlock>
<!-- The AdornedElementPlaceholder element specifies where the control being adorned should be placed. -->
<AdornedElementPlaceholder/>
</DockPanel>
</ControlTemplate>
In addition, the error message may also be displayed using a ToolTip.
<Style x:Key="textStyleTextBox" TargetType="TextBox">
<Setter Property="Foreground" Value="#333333" />
<Setter Property="MaxLength" Value="40" />
<Setter Property="Width" Value="392" />
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding (Validation.Errors).CurrentItem.ErrorContent, RelativeSource={RelativeSource Self}}" />
</Trigger>
</Style.Triggers>
</Style>
2.5. Binding path
If the binding source is an object, use the Binding.Path property to specify the value to use for the binding. If binding to XML data, use the Binding.XPath property to specify the value.
Use the Path
property to specify the source value to bind to:
-
In the simplest case, the
Path
property value is the name of the property of the source object to use for the binding, such asPath=PropertyName
. -
Subproperties of a property can be specified by a similar syntax as in C#.
For instance, the clause
Path=ShoppingCart.Order
sets the binding to the subpropertyOrder
of the object or propertyShoppingCart
. -
To bind to an attached property, place parentheses around the attached property.
For example, to bind to the attached property
DockPanel.Dock
, the syntax isPath=(DockPanel.Dock)
. -
Indexers of a property can be specified within square brackets following the property name where the indexer is applied.
For instance, the clause
Path=ShoppingCart[0]
sets the binding to the index that corresponds to how your property’s internal indexing handles the literal string "0".Nested indexers are also supported.
-
Indexers and subproperties can be mixed in a
Path
clause; for example,Path=ShoppingCart.ShippingInfo[MailingAddress,Street]
. -
Inside indexers, there can be multiple indexer parameters separated by commas (
,
). The type of each parameter can be specified with parentheses.For example,
Path="[(sys:Int32)42,(sys:Int32)24]"
, wheresys
is mapped to theSystem
namespace. -
When the source is a collection view, the current item can be specified with a slash (
/
).For example, the clause
Path=/
sets the binding to the current item in the view.When the source is a collection, this syntax specifies the current item of the default collection view.
-
Property names and slashes can be combined to traverse properties that are collections.
For example,
Path=/Offices/ManagerName
specifies the current item of the source collection, which contains anOffices
property that is also a collection. Its current item is an object that contains aManagerName
property. -
Optionally, a period (
.
) path can be used to bind to the current source.For example,
Text="{Binding}"
is equivalent toText="{Binding Path=.}"
.
2.6. Binding source
Using the DataContext property on a parent element is useful when binding multiple properties to the same source. However, sometimes it may be more appropriate to specify the binding source on individual binding declarations.
<DockPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:SDKSample">
<DockPanel.Resources>
<c:MyData x:Key="myDataSource"/>
</DockPanel.Resources>
<DockPanel.DataContext>
<Binding Source="{StaticResource myDataSource}"/>
</DockPanel.DataContext>
<Button Background="{Binding Path=ColorName}"
Width="150" Height="30">
I am bound to be RED!
</Button>
</DockPanel>
<DockPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:SDKSample">
<DockPanel.Resources>
<c:MyData x:Key="myDataSource"/>
</DockPanel.Resources>
<Button Background="{Binding Source={StaticResource myDataSource}, Path=ColorName}"
Width="150" Height="30">
I am bound to be RED!
</Button>
</DockPanel>
The Binding.ElementName and Binding.RelativeSource properties also be used to set the source of the binding explicitly.
<StackPanel>
<Slider
Name="RectangleHeight"
Width="100"
HorizontalAlignment="Left"
Maximum="72"
Minimum="5"
Orientation="Horizontal"
Value="16" />
<Button FontSize="{Binding ElementName=RectangleHeight, Path=Value}">Hello World!</Button>
</StackPanel>
2.7. Binding in XAML
Binding is a markup extension.
-
When using the binding extension to declare a binding, the declaration consists of a series of clauses following the Binding keyword and separated by commas (,).
-
The clauses in the binding declaration can be in any order and there are many possible combinations.
-
The clauses are
Name=Value
pairs, whereName
is the name of the Binding property andValue
is the value for the property.
When creating binding declaration strings in markup, they must be attached to the specific dependency property of a target object.
<TextBlock Text="{Binding Source={StaticResource myDataSource}, Path=Name}"/>
Object element syntax is an alternative to creating the binding declaration. In most cases, there’s no particular advantage to using either the markup extension or the object element syntax.
<TextBlock>
<TextBlock.Text>
<Binding Source="{StaticResource myDataSource}" Path="Name"/>
</TextBlock.Text>
</TextBlock>
2.8. Binding in code
Another way to specify a binding is to set properties directly on a Binding object in code, and then assign the binding to a property.
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// Make a new data source object
var personDetails = new Person()
{
Name = "John",
Birthdate = DateTime.Parse("2001-02-03")
};
// New binding object using the path of 'Name' for whatever source object is used
var nameBindingObject = new Binding("Name");
// Configure the binding
nameBindingObject.Mode = BindingMode.OneWay;
nameBindingObject.Source = personDetails;
nameBindingObject.Converter = NameConverter.Instance;
nameBindingObject.ConverterCulture = new CultureInfo("en-US");
// Set the binding to a target object. The TextBlock.Name property on the NameBlock UI element
BindingOperations.SetBinding(NameBlock, TextBlock.TextProperty, nameBindingObject);
}
3. Property system
Windows Presentation Foundation (WPF) provides a set of services that can be used to extend the functionality of a type’s property. Collectively, these services are referred to as the WPF property system.
-
A property that’s backed by the WPF property system is known as a dependency property.
-
Attached properties are a XAML concept, dependency properties are a WPF concept.
-
Attached properties enable extra property/value pairs to be set on any XAML element that derives from DependencyObject, even though the element doesn’t define those extra properties in its object model.
3.1. Dependency properties and CLR properties
WPF properties are typically exposed as standard .NET properties. The purpose of dependency properties is to provide a way to compute the value of a property based on the value of other inputs, such as:
-
System properties, such as themes and user preference.
-
Just-in-time property determination mechanisms, such as data binding and animations/storyboards.
-
Multiple-use templates, such as resources and styles.
-
Values known through parent-child relationships with other elements in the element tree.
Also, a dependency property can provide:
-
Self-contained validation.
-
Default values.
-
Callbacks that monitor changes to other properties.
-
A system that can coerce property values based on runtime information.
Derived classes can change some characteristics of an existing property by overriding the metadata of a dependency property, rather than overriding the actual implementation of existing properties or creating new properties.
3.2. Dependency properties back CLR properties
Dependency properties and the WPF property system extend property functionality by providing a DependencyProperty type that backs a property, as an alternative to the standard pattern of backing a property with a private field.
Here’s some commonly used terminology:
-
Dependency property, which is a property that’s backed by a DependencyProperty.
-
Dependency property identifier, which is a
DependencyProperty
instance obtained as a return value when registering a dependency property, and then stored as a static member of a class. Many of the APIs that interact with the WPF property system use the dependency property identifier as a parameter. -
CLR "wrapper", which is the get and set implementations for the property. These implementations incorporate the dependency property identifier by using it in the
GetValue
andSetValue
calls. In this way, the WPF property system provides the backing for the property.
The following example defines the IsSpinning
dependency property to show the relationship of the DependencyProperty
identifier to the property that it backs.
public static readonly DependencyProperty IsSpinningProperty = DependencyProperty.Register(
"IsSpinning", typeof(bool),
typeof(MainWindow)
);
public bool IsSpinning
{
get => (bool)GetValue(IsSpinningProperty);
set => SetValue(IsSpinningProperty, value);
}
The naming convention of the property and its backing DependencyProperty
field is important. The name of the field is always the name of the property, with the suffix Property
appended.
3.3. Attached properties
Although any object can set an attached property value, that doesn’t mean setting a value will produce a tangible result or the value will be used by another object.
Attached property usage typically follows one of these models:
-
The type that defines the attached property is the parent of the elements that set values for the attached property. The parent type iterates its child objects through internal logic that acts on the object tree structure, obtains the values, and acts on those values in some manner.
-
The type that defines the attached property is used as the child element for various possible parent elements and content models.
-
The type that defines the attached property represents a service. Other types set values for the attached property. Then, when the element that set the property is evaluated in the context of the service, the attached property values are obtained through internal logic of the service class.
Attached properties in WPF don’t have the typical CLR get
and set
wrapper methods because the properties might be set from outside of the CLR namespace.
-
To permit a XAML processor to set those values when parsing XAML, the class that defines the attached property must implement dedicated accessor methods in the form of
Get<property name>
andSet<property name>
.// Attached properties in code DockPanel myDockPanel = new(); TextBox myTextBox = new(); myTextBox.Text = "Enter text"; // Add child element to the DockPanel. myDockPanel.Children.Add(myTextBox); // Set the attached property value. DockPanel.SetDock(myTextBox, Dock.Top);
public static Dock GetDock(UIElement element); public static void SetDock(UIElement element, Dock dock);
<!-- Attached properties in XAML --> <DockPanel> <TextBox DockPanel.Dock="Top">Enter text</TextBox> </DockPanel>
Define attached property as a dependency in the defining class by declaring a public static readonly
field of type DependencyProperty
.
-
Then, assign the return value of the
RegisterAttached
method to the field, which is also known as the dependency property identifier. -
Follow the WPF property naming convention that distinguishes fields from the properties that they represent, by naming the identifier field
<property name>Property
. -
Also, provide static
Get<property name>
andSet<property name>
accessor methods, which lets the property system access the attached property.public class Aquarium : UIElement { // Register an attached dependency property with the specified // property name, property type, owner type, and property metadata. public static readonly DependencyProperty HasFishProperty = DependencyProperty.RegisterAttached( "HasFish", typeof(bool), typeof(Aquarium), new FrameworkPropertyMetadata(defaultValue: false, flags: FrameworkPropertyMetadataOptions.AffectsRender) ); // Declare a get accessor method. public static bool GetHasFish(UIElement target) => (bool)target.GetValue(HasFishProperty); // Declare a set accessor method. public static void SetHasFish(UIElement target, bool value) => target.SetValue(HasFishProperty, value); }
-
The
get
accessor method signature ispublic static object Get<property name>(DependencyObject target)
, where:-
target
is theDependencyObject
from which the attached property is read.The target type can be more specific than
DependencyObject
. For example, theDockPanel.GetDock
accessor method types the target asUIElement
because the attached property is intended to be set onUIElement
instances. -
The return type can be more specific than
object
. For example, theGetDock
method types the returned value asDock
because the return value should be aDock
enumeration.
-
-
The
set
accessor method signature ispublic static void Set<property name>(DependencyObject target, object value)
, where:-
target
is theDependencyObject
on which the attached property is written.The
target
type can be more specific thanDependencyObject
. For example, theSetDock
method types the target asUIElement
because the attached property is intended to be set onUIElement
instances. -
The
value
type can be more specific thanobject
. For example, theSetDock
method requires aDock
value.
-
3.4. Property value inheritance
Property value inheritance is a feature of the Windows Presentation Foundation (WPF) property system and applies to dependency properties.
-
Property value inheritance lets child elements in a tree of elements obtain the value of a particular property from the nearest parent element.
-
Since a parent element might also have obtained its property value through property value inheritance, the system potentially recurses back to the page root.
-
The WPF property system doesn’t enable property value inheritance by default, and value inheritance is inactive unless specifically enabled in dependency property metadata.
-
Even with property value inheritance enabled, a child element will only inherit a property value in the absence of a higher precedence value.
4. Routed events
Windows Presentation Foundation (WPF) application developers and component authors can use routed events to propagate events through an element tree, and invoke event handlers on multiple listeners in the tree.
-
From a functional perspective, a routed event is a type of event that can invoke handlers on multiple listeners in an element tree, not just on the event source.
-
An event listener is the element where an event handler is attached and invoked.
-
An event source is the element or object that originally raised an event.
-
-
From an implementation perspective, a routed event is an event registered with the WPF event system, backed by an instance of the RoutedEvent class, and processed by the WPF event system.
-
Typically, a routed event is implemented with a CLR event "wrapper" to enable attaching handlers in XAML and in code-behind as you would a CLR event.
-
Depending on how a routed event is defined, when the event is raised on a source element it:
-
Bubbles up through element tree from the source element to the root element, which is typically a page or window.
-
Tunnels down through the element tree from the root element to the source element.
-
Doesn’t travel through the element tree, and only occurs on the source element directly.
4.1. Routed event and event handler
A routed event is an event registered with the WPF event system, backed by an instance of the RoutedEvent
class, and processed by the WPF event system.
-
The
RoutedEvent
instance, obtained from registration, is typically stored as apublic static readonly
member of the "owner" class, that registered it. -
Typically, a routed event implements an identically named CLR event "wrapper" that is similar to how a dependency property is a CLR property.
-
The CLR event wrapper contains
add
andremove
accessors to enable attaching handlers in XAML and in code-behind through language-specific event syntax. -
The
add
andremove
accessors override their CLR implementation and call the routed event AddHandler and RemoveHandler methods.// Register a custom routed event using the Bubble routing strategy. public static readonly RoutedEvent TapEvent = EventManager.RegisterRoutedEvent( name: "Tap", routingStrategy: RoutingStrategy.Bubble, handlerType: typeof(RoutedEventHandler), ownerType: typeof(CustomButton)); // Provide CLR accessors for adding and removing an event handler. public event RoutedEventHandler Tap { add { AddHandler(TapEvent, value); } remove { RemoveHandler(TapEvent, value); } }
-
In XAML, attach an event handler to an element by declaring the event name as an attribute on the event listener element.
-
The attribute value is the handler method name.
-
The handler method must be implemented in the code-behind partial class for the XAML page.
-
The event listener is the element where the event handler is attached and invoked.
-
If the event isn’t a member of the listener’s class, use the qualified event name in the form of
<owner type>.<event name>
.<StackPanel Button.Click="YesNoCancelButton_Click"> <Button Name="YesButton" Click="YesButtonClick">Yes</Button> <Button Name="NoButton" Click="NoButtonClick">No</Button> <Button Name="CancelButton" Click="CancelClick">Cancel</Button> </StackPanel>
-
The signature of the event handler method in code-behind must match the delegate type for the routed event.
private void YesNoCancelButtonClick(object sender, RoutedEventArgs e) { }
To attach an event handler for a routed event to an element using code:
-
Directly call the
AddHandler
method.// Routed event handlers can always be attached this way. Button1.AddHandler(ButtonBase.ClickEvent, new RoutedEventHandler(Button_Click)); StackPanel1.AddHandler(ButtonBase.ClickEvent, new RoutedEventHandler(Button_Click));
-
If the routed event implements a CLR event wrapper, use language-specific event syntax to add event handlers.
Button1.Click += Button_Click;
4.2. Attached events
WPF attached events are implemented as routed events backed by a RoutedEvent field.
public class AquariumFilter
{
// Register a custom routed event using the bubble routing strategy.
public static readonly RoutedEvent CleanEvent = EventManager.RegisterRoutedEvent(
"Clean", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AquariumFilter));
// Provide an add handler accessor method for the Clean event.
public static void AddCleanHandler(DependencyObject dependencyObject, RoutedEventHandler handler)
{
if (dependencyObject is not UIElement uiElement) { return; }
uiElement.AddHandler(CleanEvent, handler);
}
// Provide a remove handler accessor method for the Clean event.
public static void RemoveCleanHandler(DependencyObject dependencyObject, RoutedEventHandler handler)
{
if (dependencyObject is not UIElement uiElement) { return; }
uiElement.RemoveHandler(CleanEvent, handler);
}
}
-
An
Add<event name>Handler
method, with a first parameter that’s the element on which the event handler is attached, and a second parameter that’s the event handler to add.-
The method must be
public
andstatic
, with no return value. -
The method calls the
AddHandler
base class method, passing in the routed event and handler as arguments.-
This method supports the XAML attribute syntax for attaching an event handler to an element.
-
This method also enables code access to the event handler store for the attached event.
-
-
-
A
Remove<event name>Handler
method, with a first parameter that’s the element on which the event handler is attached, and a second parameter that’s the event handler to remove.-
The method must be
public
andstatic
, with no return value. -
The method calls the
RemoveHandler
base class method, passing in the routed event and handler as arguments.-
This method enables code access to the event handler store for the attached event.
-
-
-
WPF implements attached events as routed events because the identifier for a
RoutedEvent
is defined by the WPF event system. -
The RegisterRoutedEvent method that returns the attached event identifier is the same method used to register non-attached routed events.
-
Unlike the CLR event "wrapper" used to back non-attached routed events, the attached event accessor methods can be implemented in classes that don’t derive from UIElement or ContentElement.
-
It is possible because the attached event backing code calls the UIElement.AddHandler and UIElement.RemoveHandler methods on a passed in
UIElement
instance. -
In contrast, the CLR wrapper for non-attached routed events calls those methods directly on the owning class, so that class must derive from
UIElement
.
-
-
When defining a custom attached event using the WPF model of basing attached events on routed events, use the UIElement.RaiseEvent method to raise an attached event on any
UIElement
orContentElement
.-
When raising a routed event, whether it’s attached or not, an element is required to designate in the element tree as the event source.
-
That source is then reported as the
RaiseEvent
caller. For example, to raise theAquariumFilter.Clean
attached routed event onaquarium1
:aquarium1.RaiseEvent(new RoutedEventArgs(AquariumFilter.CleanEvent));
-
In XAML syntax, an attached event is specified by its event name and its owner type, in the form of <owner type>.<event name>
.
-
Because the event name is qualified with the name of its owner type, the syntax allows the event to be attached to any element that can be instantiated.
-
It is also applicable to handlers for regular routed events that attach to an arbitrary element along the event route.
<!-- attaches the AquariumFilter_Clean handler for the AquariumFilter.Clean attached event to the aquarium1 element --> <aqua:Aquarium x:Name="aquarium1" Height="300" Width="400" aqua:AquariumFilter.Clean="AquariumFilter_Clean"/>
-
Event handlers can also be attached for attached events in code behind, by calling the AddHandler method on the object that the handler should attach to and pass the event identifier and handler as parameters to the method.
aquarium1.AddHandler(AquariumFilter.Clean, new RoutedEventHandler(AquariumFilter_Clean), true);
4.3. WPF input events
-
By convention, WPF routed events that follow a tunneling route have a name that’s prefixed with "Preview".
-
Input events often come in pairs, with one being a preview event and the other a bubbling routed event.
-
The
Preview
prefix signifies that the preview event completes before the paired bubbling event starts. -
A preview input event that’s marked as handled won’t invoke any normally registered event handlers for the remainder of the preview route, and the paired bubbling event won’t be raised.
The order of event processing following a mouse-down action on leaf element #2 is:
-
PreviewMouseDown
tunneling event on the root element. -
PreviewMouseDown
tunneling event on intermediate element #1. -
PreviewMouseDown
tunneling event on leaf element #2, which is the source element. -
MouseDown
bubbling event on leaf element #2, which is the source element. -
MouseDown
bubbling event on intermediate element #1. -
MouseDown
bubbling event on the root element.
4.4. Weak event patterns
Listening for events can lead to memory leaks.
-
The typical technique for listening to an event is to use the language-specific syntax that attaches a handler to an event on a source.
-
For example, in C#, that syntax is:
source.SomeEvent += new SomeEventHandler(MyEventHandler)
that creates a strong reference from the event source to the event listener. -
Ordinarily, attaching an event handler for a listener causes the listener to have an object lifetime that is influenced by the object lifetime of the source (unless the event handler is explicitly removed).
-
Whenever the source object lifetime extends beyond the object lifetime of the listener, the normal event pattern leads to a memory leak: the listener is kept alive longer than intended.
-
The weak event pattern can be used whenever a listener needs to register for an event, but the listener does not explicitly know when to unregister, and can also be used whenever the object lifetime of the source exceeds the useful object lifetime of the listener.
5. Commands
A command can be used to:
-
separate the semantics and the object that invokes a command from the logic that executes the command.
-
indicate whether an action is possible by implementing the
CanExecute
method.
The routed command model in WPF can be broken up into four main concepts:
-
The command is the action to be executed.
-
The command source is the object which invokes the command.
-
The command target is the object that the command is being executed on.
-
The command binding is the object which maps the command logic to the command.
A commands is created by implementing the ICommand interface.
-
Execute
method performs the actions that are associated with the command. -
CanExecute
method determines whether the command can execute on the current command target. -
CanExecuteChanged
event is raised if the command manager that centralizes the commanding operations detects a change in the command source that might invalidate a command that has been raised but not yet executed by the command binding.
The RelayCommand and RelayCommand<T> are
|
A command source is the object that generally implements the ICommandSource interface, which invokes the command.
-
Examples of command sources are
MenuItem
,Button
, andKeyGesture
. -
A command source like a
Button
can subscribe to theCanExecuteChanged
event and be disabled ifCanExecute
returnsfalse
or be enabled ifCanExecute
returnstrue
. -
Command
is the command to execute when the command source is invoked. -
CommandTarget
is the object on which to execute the command, which is only applicable when theICommand
is aRoutedCommand
.-
If the
CommandTarget
is set on anICommandSource
and the corresponding command is not aRoutedCommand
, the command target is ignored. -
If the
CommandTarget
is not set, the element with keyboard focus will be the command target.
-
-
CommandParameter
is a user-defined data type used to pass information to the handlers implementing the command.
A CommandBinding associates a command with the event handlers that implement the command.
-
The
CommandBinding
class contains aCommand
property, andPreviewExecuted
,Executed
,PreviewCanExecute
, andCanExecute
events.<Window.CommandBindings> <CommandBinding Command="ApplicationCommands.Open" Executed="OpenCmdExecuted" CanExecute="OpenCmdCanExecute"/> </Window.CommandBindings>
// Creating CommandBinding and attaching an Executed and CanExecute handler CommandBinding OpenCmdBinding = new CommandBinding( ApplicationCommands.Open, OpenCmdExecuted, OpenCmdCanExecute); this.CommandBindings.Add(OpenCmdBinding);
void OpenCmdExecuted(object target, ExecutedRoutedEventArgs e) { String command, targetobj; command = ((RoutedCommand)e.Command).Name; targetobj = ((FrameworkElement)target).Name; MessageBox.Show("The " + command + " command has been invoked on target object " + targetobj); }
void OpenCmdCanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = true; }
A command target is the element on which the command is executed. With regards to a RoutedCommand
, the command target is the element at which routing of the Executed
and CanExecute
starts.
-
The command source can explicitly set the command target. If the command target is not defined, the element with keyboard focus will be used as the command target.
<StackPanel> <Menu> <MenuItem Command="ApplicationCommands.Paste" CommandTarget="{Binding ElementName=mainTextBox}" /> </Menu> <TextBox Name="mainTextBox"/> </StackPanel>
// Creating the UI objects StackPanel mainStackPanel = new StackPanel(); TextBox pasteTextBox = new TextBox(); Menu stackPanelMenu = new Menu(); MenuItem pasteMenuItem = new MenuItem(); // Adding objects to the panel and the menu stackPanelMenu.Items.Add(pasteMenuItem); mainStackPanel.Children.Add(stackPanelMenu); mainStackPanel.Children.Add(pasteTextBox); // Setting the command to the Paste command pasteMenuItem.Command = ApplicationCommands.Paste; // Setting the command target to the TextBox pasteMenuItem.CommandTarget = pasteTextBox;
The CommandManager serves a number of command related functions.
-
It provides a set of static methods for adding and removing
PreviewExecuted
,Executed
,PreviewCanExecute
, andCanExecute
event handlers to and from a specific element. -
It provides a means to register
CommandBinding
andInputBinding
objects onto a specific class. -
The
CommandManager
also provides a means, through the RequerySuggested event, to notify a command when it should raise theCanExecuteChanged
event. -
The InvalidateRequerySuggested method forces the
CommandManager
to raise theRequerySuggested
event, which is useful for conditions that should disable/enable a command but are not conditions that theCommandManager
is aware of.
6. Windows
In WPF, a window is encapsulated by the Window class that used to do the following:
-
Display a window.
-
Configure the size, position, and appearance of a window.
-
Host application-specific content.
-
Manage the lifetime of a window.
A window is divided into two areas: the non-client area and client area.
The non-client area of a window is implemented by WPF and includes the parts of a window that are common to most windows, including the following:
-
A title bar (1-5).
-
An icon (1).
-
Title (2).
-
Minimize (3), Maximize (4), and Close (5) buttons.
-
System menu (6) with menu items. Appears when clicking on the icon (1).
-
Border (7).
The client area of a window is the area within a window’s non-client area and is used by developers to add application-specific content, such as menu bars, tool bars, and controls.
-
Client area (8).
-
Resize grip (9). This is a control added to the Client area (8).
For a window that is defined using both XAML markup and code-behind:
-
XAML markup files are configured as MSBuild
Page
items. -
Code-behind files are configured as MSBuild
Compile
items.
.NET SDK projects automatically import the correct Page
and Compile
items. When the project is configured for WPF, the XAML markup files are automatically imported as Page
items, and the corresponding code-behind file is imported as Compile
.
6.1. Lifetime
A window that is opened by using the Show
method doesn’t have an implicit relationship with the window that created it. Users can interact with either window independently of the other, which means that either window can do the following:
-
Cover the other (unless one of the windows has its
Topmost
property set totrue
). -
Be minimized, maximized, and restored without affecting the other.
After ownership by setting the Owner
property of the owned window with a reference to the owner window is established:
-
The owned window can reference its owner window by inspecting the value of its
Owner
property. -
The owner window can discover all the windows it owns by inspecting the value of its
OwnedWindows
property.
A window opened by calling Show
is a modeless window, and the application doesn’t prevent users from interacting with other windows in the application.
Opening a window with ShowDialog
opens a window as modal and restricts user interaction to the specific window.
The life of a window starts coming to an end when a user closes it. Once a window is closed, it can’t be reopened.
A window can be closed by using elements in the non-client area, including the following:
-
The
Close
item of the System menu. -
Pressing
ALT + F4
.public MainWindow() { InitializeComponent(); KeyDown += (s, e) => { // inhibit the ALT + F4 e.Handled = e.SystemKey == Key.F4 && Keyboard.Modifiers == ModifierKeys.Alt; }; }
-
Pressing the
Close
button. -
Pressing
ESC
when a button has theIsCancel
property set totrue
on a modal window.
The following illustration shows the sequence of the principal events in the lifetime of a window:
The following illustration shows the sequence of the principal events in the lifetime of a window that is shown without activation (ShowActivated
is set to false
before the window is shown):
6.2. Appearance
To configure the non-client area, Window provides several properties, which include Icon
to set a window’s icon and Title
to set its title.
The appearance and behavior of non-client area border can also be changed by configuring a window’s resize mode, window style, and whether it appears as a button in the desktop task bar.
<!-- Non-rectangular window style -->
<Window x:Class="WindowsOverview.ClippedWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="ClippedWindow" SizeToContent="WidthAndHeight"
WindowStyle="None" AllowsTransparency="True" Background="Transparent">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="20"/>
</Grid.RowDefinitions>
<Rectangle Stroke="#FF000000" RadiusX="10" RadiusY="10"/>
<Path Fill="White" Stretch="Fill" Stroke="#FF000000" HorizontalAlignment="Left" Margin="15,-5.597,0,-0.003" Width="30" Grid.Row="1" Data="M22.166642,154.45381 L29.999666,187.66699 40.791059,154.54395"/>
<Rectangle Fill="White" RadiusX="10" RadiusY="10" Margin="1"/>
<TextBlock HorizontalAlignment="Left" VerticalAlignment="Center" FontSize="25" Text="Greetings!" TextWrapping="Wrap" Margin="5,5,50,5"/>
<Button HorizontalAlignment="Right" VerticalAlignment="Top" Background="Transparent" BorderBrush="{x:Null}" Foreground="Red" Content="❌" FontSize="15" />
<Grid.Effect>
<DropShadowEffect BlurRadius="10" ShadowDepth="3" Color="LightBlue"/>
</Grid.Effect>
</Grid>
</Window>
// Hide the Minimize, Maximize, and Close buttons
public MainWindow()
{
InitializeComponent();
SourceInitialized += (s, e) =>
{
const int WM_SYSTEM_MENU = 0x80000;
const int WM_GW_STYLE = -16;
var hWnd = new System.Windows.Interop.WindowInteropHelper(this).Handle;
if (hWnd == IntPtr.Zero)
{
throw new InvalidOperationException("The window has not yet been completely initialized");
}
// Hide the Minimize, Maximize, and Close buttons
SetWindow(hWnd, WM_GW_STYLE, GetWindow(hWnd, WM_GW_STYLE) & ~WM_SYSTEM_MENU);
};
}
[DllImport("user32.dll", EntryPoint = "GetWindowLong")]
private static extern int GetWindow(IntPtr hWnd, int nIndex);
[DllImport("user32.dll", EntryPoint = "SetWindowLong", SetLastError = true)]
private static extern int SetWindow(IntPtr hWnd, int nIndex, int dwNew);
6.3. Dialog boxes
When designing a dialog box, follow these suggestions to create a good user experience:
❌ DON’T clutter the dialog window. The dialog experience is for the user to enter some data, or to make a choice.
✔️ DO provide an OK
button to close the window.
✔️ DO set the OK
button’s IsDefault
property to true
to allow the user to press the ENTER
key to accept and close the window.
✔️ CONSIDER adding a Cancel
button so that the user can close the window and indicate that they don’t want to continue.
✔️ DO set the Cancel
button’s IsCancel
property to true
to allow the user to press the ESC
key to close the window.
✔️ DO set the title of the window to accurately describe what the dialog represents, or what the user should do with the dialog.
✔️ DO set minimum width and height values for the window, preventing the user from resizing the window too small.
✔️ CONSIDER disabling the ability to resize the window if ShowInTaskbar
is set to false
.
✔️ DO When a menu item or button runs a function that requires user interaction through a dialog box before the function can continue, the control should use an ellipsis at the end of its header text:
<MenuItem Header="_Open..." Click="openMenuItem_Click" />
<!-- or -->
<Button Content="_Save As..." Click="saveAsButton_Click" />
✔️ DO When a menu item or button runs a function that displays a dialog box that does NOT require user interaction, such as an About dialog box, an ellipsis isn’t required.
6.4. Multiple windows, multiple threads
Typically, WPF applications start with two threads: one for handling rendering and another for managing the UI.
-
The rendering thread effectively runs hidden in the background while the UI thread receives input, handles events, paints the screen, and runs application code.
-
Most applications use a single UI thread, although in some situations it is best to use several.
-
The UI thread queues work items inside an object called a Dispatcher that selects work items on a priority basis and runs each one to completion.
-
Every UI thread must have at least one
Dispatcher
, and eachDispatcher
can execute work items in exactly one thread. -
The trick to building responsive, user-friendly applications is to maximize the
Dispatcher
throughput by keeping the work items small.
-
Most classes derive from DispatcherObject that stores a reference to the
Dispatcher
linked to the currently running thread that creates it at construction.-
A
DispatcherObject
can call its publicVerifyAccess
method that examines theDispatcher
associated with the current thread and compares it to theDispatcher
reference stored during construction, and if they don’t match,VerifyAccess
throws an exception. -
VerifyAccess
is intended to be called at the beginning of every method belonging to aDispatcherObject
.
-
-
A background thread can ask the UI thread to perform an operation on its behalf by registering a work item with the
Dispatcher
of the UI thread.-
The
Dispatcher
class provides the methods for registering work items:InvokeAsync
,BeginInvoke
, andInvoke
. -
Invoke
is a synchronous call – that is, it doesn’t return until the UI thread actually finishes executing the delegate. -
InvokeAsync
andBeginInvoke
are asynchronous and return immediately.
-
-
The
Dispatcher
orders the elements in its queue by priority that maintained in theDispatcherPriority
enumeration. -
WPF application may require multiple top-level windows to do a better job, which is especially true if there’s any chance that one of the windows will monopolize the thread.
private void NewThreadWindow_Click(object sender, RoutedEventArgs e) { Thread newWindowThread = new Thread(ThreadStartingPoint); newWindowThread.SetApartmentState(ApartmentState.STA); newWindowThread.IsBackground = true; newWindowThread.Start(); } private void ThreadStartingPoint() { new MultiWindow().Show(); System.Windows.Threading.Dispatcher.Run(); }
-
Windows Explorer works in multiple top-level windows within multiple threads fashion.
-
Each new Explorer window belongs to the original process, but it’s created under the control of an independent thread.
-
When Explorer becomes nonresponsive, such as when looking for network resources, other Explorer windows continue to be responsive and usable.
-
7. Styles, templates, and triggers
7.1. Styles
A Style, commonly declared as a resource, can apply a set of property values to one or more elements.
-
When setting the TargetType of a style and omit the
x:Key
attribute, the style is applied to all theTargetType
elements scoped to the style, which is generally the XAML file itself.<Window.Resources> <!--A Style that affects all TextBlocks--> <Style TargetType="TextBlock"> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="FontFamily" Value="Comic Sans MS"/> <Setter Property="FontSize" Value="14"/> </Style> </Window.Resources>
-
If adding an
x:Key
attribute with value to the style, the style is no longer implicitly applied to all elements ofTargetType
. Only elements that explicitly reference the style will have the style applied to them.<Window.Resources> <Style x:Key="TitleText" TargetType="TextBlock"> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="FontFamily" Value="Comic Sans MS"/> <Setter Property="FontSize" Value="14"/> </Style> </Window.Resources>
<StackPanel> <TextBlock Style="{StaticResource TitleText}">My Pictures</TextBlock> <TextBlock>Check out my new pictures!</TextBlock> </StackPanel>
-
To assign a named style to an element programmatically, get the style from the resources collection and assign it to the element’s
Style
property.textblock1.Style = (Style)Resources["TitleText"];
-
A style can extend another style with the
BaseOn
property.<Window.Resources> <!-- .... other resources .... --> <!--A Style that affects all TextBlocks--> <Style TargetType="TextBlock"> <!-- x:Key is implicitly set to {x:Type TextBlock} --> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="FontFamily" Value="Comic Sans MS"/> <Setter Property="FontSize" Value="14"/> </Style> <!--A Style that extends the previous TextBlock Style with an x:Key of TitleText--> <Style BasedOn="{StaticResource {x:Type TextBlock}}" TargetType="TextBlock" x:Key="TitleText"> <Setter Property="FontSize" Value="26"/> <Setter Property="Foreground"> <Setter.Value> <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1"> <LinearGradientBrush.GradientStops> <GradientStop Offset="0.0" Color="#90DDDD" /> <GradientStop Offset="1.0" Color="#5BFFFF" /> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Setter.Value> </Setter> </Style> </Window.Resources>
-
The
x:Key
of a style is implicitly set to{x:Type TargetType}
.It means that if explicitly setting the
x:Key
value to anything other than{x:Type TargetType}
, theStyle
isn’t applied to allTargetType
elements automatically. -
If
TargetType
isn’t specified, the properties must be qualified in theSetter
objects with a class name by using the syntaxProperty="ClassName.Property"
. -
Also note that many WPF controls consist of a combination of other WPF controls. If creating a style that applies to all controls of a type, unexpected results might happen.
For example, if creating a style that targets the
TextBlock
type in aWindow
, the style is applied to allTextBlock
controls in the window, even if theTextBlock
is part of another control, such as aListBox
.
7.2. Control templates
In WPF, the ControlTemplate of a control, Commonly declared as a resource, defines the appearance of the control.
-
Each control has a default template assigned to the Control.Template property.
-
A control template rewrites the visual appearance of the entire control, while a style simply applies property changes to the existing control.
However, since the template of a control is applied by setting the
Control.Template
property, a template can be defined or set using a style. -
A TemplateBinding is an optimized form of a binding for template scenarios, analogous to a binding constructed with
{Binding RelativeSource={RelativeSource TemplatedParent}}
, such as for binding parts of the template to properties of the control. -
If a ContentPresenter is declared in the
ControlTemplate
of a ContentControl, theContentPresenter
will automatically bind to theContentTemplate
and Content properties.Likewise, an ItemsPresenter that is in the
ControlTemplate
of an ItemsControl will automatically bind to the ItemTemplate and Items properties.<UserControl> <UserControl.Resources> <!-- Defined a ControlTemplate as a resource --> <ControlTemplate x:Key="roundbutton" TargetType="Button"> <Grid> <Ellipse Fill="{TemplateBinding Background}" Stroke="{TemplateBinding Foreground}" /> <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" /> </Grid> </ControlTemplate> </UserControl.Resources> <StackPanel> <!-- Set the button's Template property to the roundbutton resource --> <Button Template="{StaticResource roundbutton}">Hello</Button> <!-- Defined the ControlTemplate inline --> <Button Background="Red" Foreground="White"> <Button.Content>World</Button.Content> <Button.Template> <ControlTemplate TargetType="Button"> <Grid> <Ellipse Fill="{TemplateBinding Background}" Stroke="{TemplateBinding Foreground}" /> <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" /> </Grid> </ControlTemplate> </Button.Template> </Button> </StackPanel> </UserControl>
7.3. Data templates
In WPF, the DataTemplate is used to custom the presentation and appearance of the data objects.
-
In most cases, all other aspects of presentation, such as what an item looks like when it is selected or how a
ListBox
lays out the items, do not belong in the definition of aDataTemplate
.<!-- Defined the DataTemplate inline --> <ListBox Width="400" Margin="10" ItemsSource="{Binding Source={StaticResource myTodoList}}"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel> <TextBlock Text="{Binding Path=TaskName}" /> <TextBlock Text="{Binding Path=Description}"/> <TextBlock Text="{Binding Path=Priority}"/> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox>
<!-- Defined a DataTemplate as a resource --> <UserControl.Resources> <DataTemplate x:Key="myTaskTemplate"> <StackPanel> <TextBlock Text="{Binding Path=TaskName}" /> <TextBlock Text="{Binding Path=Description}" /> <TextBlock Text="{Binding Path=Priority}" /> </StackPanel> </DataTemplate> </UserControl.Resources> <StackPanel> <ListBox Width="400" Margin="10" ItemTemplate="{StaticResource myTaskTemplate}" ItemsSource="{Binding Source={StaticResource myTodoList}}" /> </StackPanel>
-
The
DataTemplate
class has aDataType
property that is very similar to theTargetType
property of theStyle
class.<DataTemplate DataType="{x:Type local:Task}"> <StackPanel> <TextBlock Text="{Binding Path=TaskName}" /> <TextBlock Text="{Binding Path=Description}"/> <TextBlock Text="{Binding Path=Priority}"/> </StackPanel> </DataTemplate>
-
To supply logic to choose which
DataTemplate
to use based on thePriority
value of the data object, create a subclass of DataTemplateSelector and override the SelectTemplate method.namespace SDKSample { public class TaskListDataTemplateSelector : DataTemplateSelector { public override DataTemplate SelectTemplate(object item, DependencyObject container) { FrameworkElement element = container as FrameworkElement; if (element != null && item != null && item is Task) { Task taskitem = item as Task; if (taskitem.Priority == 1) return element.FindResource("importantTaskTemplate") as DataTemplate; else return element.FindResource("myTaskTemplate") as DataTemplate; } return null; } } }
<Window.Resources> <local:TaskListDataTemplateSelector x:Key="myDataTemplateSelector" /> </Window.Resources>
<ListBox Width="400" Margin="10" ItemsSource="{Binding Source={StaticResource myTodoList}}" ItemTemplateSelector="{StaticResource myDataTemplateSelector}" HorizontalContentAlignment="Stretch"/>
-
Styling and Templating an ItemsControl
<ItemsControl Margin="10" ItemsSource="{Binding Source={StaticResource myTodoList}}"> <!--The ItemsControl has no default visual appearance. Use the Template property to specify a ControlTemplate to define the appearance of an ItemsControl. The ItemsPresenter uses the specified ItemsPanelTemplate (see below) to layout the items. If an ItemsPanelTemplate is not specified, the default is used. (For ItemsControl, the default is an ItemsPanelTemplate that specifies a StackPanel.--> <ItemsControl.Template> <ControlTemplate TargetType="ItemsControl"> <Border BorderBrush="Aqua" BorderThickness="1" CornerRadius="15"> <ItemsPresenter/> </Border> </ControlTemplate> </ItemsControl.Template> <!--Use the ItemsPanel property to specify an ItemsPanelTemplate that defines the panel that is used to hold the generated items. In other words, use this property if you want to affect how the items are laid out.--> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <WrapPanel /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <!--Use the ItemTemplate to set a DataTemplate to define the visualization of the data objects. This DataTemplate specifies that each data object appears with the Proriity and TaskName on top of a silver ellipse.--> <ItemsControl.ItemTemplate> <DataTemplate> <DataTemplate.Resources> <Style TargetType="TextBlock"> <Setter Property="FontSize" Value="18"/> <Setter Property="HorizontalAlignment" Value="Center"/> </Style> </DataTemplate.Resources> <Grid> <Ellipse Fill="Silver"/> <StackPanel> <TextBlock Margin="3,3,3,0" Text="{Binding Path=Priority}"/> <TextBlock Margin="3,0,3,7" Text="{Binding Path=TaskName}"/> </StackPanel> </Grid> </DataTemplate> </ItemsControl.ItemTemplate> <!--Use the ItemContainerStyle property to specify the appearance of the element that contains the data. This ItemContainerStyle gives each item container a margin and a width. There is also a trigger that sets a tooltip that shows the description of the data object when the mouse hovers over the item container.--> <ItemsControl.ItemContainerStyle> <Style> <Setter Property="Control.Width" Value="100"/> <Setter Property="Control.Margin" Value="5"/> <Style.Triggers> <Trigger Property="Control.IsMouseOver" Value="True"> <Setter Property="Control.ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=Content.Description}"/> </Trigger> </Style.Triggers> </Style> </ItemsControl.ItemContainerStyle> </ItemsControl>
-
The HierarchicalDataTemplate class is designed to be used with HeaderedItemsControl types to display collection data that contains other collections such as a
Menu
or aTreeView
.<Window x:Class="SDKSample.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:src="clr-namespace:SDKSample" Title="HierarchicalDataTemplate Sample"> <DockPanel> <DockPanel.Resources> <src:ListLeagueList x:Key="MyList" /> <HierarchicalDataTemplate DataType="{x:Type src:League}" ItemsSource="{Binding Path=Divisions}"> <TextBlock Text="{Binding Path=Name}" /> </HierarchicalDataTemplate> <HierarchicalDataTemplate DataType="{x:Type src:Division}" ItemsSource="{Binding Path=Teams}"> <TextBlock Text="{Binding Path=Name}" /> </HierarchicalDataTemplate> <DataTemplate DataType="{x:Type src:Team}"> <TextBlock Text="{Binding Path=Name}" /> </DataTemplate> </DockPanel.Resources> <Menu Name="menu1" Margin="10,10,10,10" DockPanel.Dock="Top"> <MenuItem Header="My Soccer Leagues" ItemsSource="{Binding Source={StaticResource MyList}}" /> </Menu> <TreeView> <TreeViewItem Header="My Soccer Leagues" ItemsSource="{Binding Source={StaticResource MyList}}" /> </TreeView> </DockPanel> </Window>
7.4. Triggers
A trigger sets properties or starts actions, such as an animation, when a property value changes or when an event is raised.
-
Style
,ControlTemplate
, andDataTemplate
all have aTriggers
property that can contain a set of triggers. -
A Trigger that sets property values or starts actions based on the value of a property is called a property trigger.
<Window.Resources> <!-- .... other resources .... --> <Style TargetType="ListBoxItem"> <Setter Property="Opacity" Value="0.5" /> <Setter Property="MaxHeight" Value="75" /> <Style.Triggers> <Trigger Property="IsSelected" Value="True"> <Trigger.Setters> <Setter Property="Opacity" Value="1.0" /> </Trigger.Setters> </Trigger> </Style.Triggers> </Style> </Window.Resources>
-
The properties changed by triggers are automatically reset to their previous value when the triggered condition is no longer satisfied.
-
Another type of trigger is the EventTrigger, which starts a set of actions based on the occurrence of an event.
<Style.Triggers> <Trigger Property="IsSelected" Value="True"> <Trigger.Setters> <Setter Property="Opacity" Value="1.0" /> </Trigger.Setters> </Trigger> <EventTrigger RoutedEvent="Mouse.MouseEnter"> <EventTrigger.Actions> <BeginStoryboard> <Storyboard> <DoubleAnimation Duration="0:0:0.2" Storyboard.TargetProperty="MaxHeight" To="90" /> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> <EventTrigger RoutedEvent="Mouse.MouseLeave"> <EventTrigger.Actions> <BeginStoryboard> <Storyboard> <DoubleAnimation Duration="0:0:1" Storyboard.TargetProperty="MaxHeight" /> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> </Style.Triggers>
-
MultiTriggers applly property values or performs actions when a set of conditions are satisfied.
<Style.Triggers> <Trigger Property="IsEnabled" Value="false"> <Setter Property="Background" Value="#EEEEEE" /> </Trigger> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="HasItems" Value="false" /> <Condition Property="Width" Value="Auto" /> </MultiTrigger.Conditions> <Setter Property="MinWidth" Value="120"/> </MultiTrigger> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="HasItems" Value="false" /> <Condition Property="Height" Value="Auto" /> </MultiTrigger.Conditions> <Setter Property="MinHeight" Value="95"/> </MultiTrigger> </Style.Triggers>
-
Event setters invoke the specified event handlers in response to routed events, which apply to all elements that reference the Style rather than requiring to attach instance handlers to each individual element.
-
Only Style.Setters support EventSetter objects.
-
Handlers attached through event setters are invoked after any class handlers for an event, and also after any instance handlers.
As a result, if a class handler or instance handler marks an event handled in its arguments, then the handler declared by an event setter is not invoked, unless the event setter specifically sets HandledEventsToo true.
<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="SDKSample.EventOvw2" Name="dpanel2" Initialized="PrimeHandledToo"> <StackPanel.Resources> <Style TargetType="{x:Type Button}"> <EventSetter Event="Click" Handler="b1SetColor"/> </Style> </StackPanel.Resources> <Button>Click me</Button> <Button Name="ThisButton" Click="HandleThis"> Raise event, handle it, use handled=true handler to get it anyway. </Button> </StackPanel>
void b1SetColor(object sender, RoutedEventArgs e) { Button b = e.Source as Button; b.Background = new SolidColorBrush(Colors.Azure); } void HandleThis(object sender, RoutedEventArgs e) { e.Handled=true; }
-