WPF Applications
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and was checked on 2026-10-19. WPF is a Windows-only
UI framework, and the machine that built these examples is a Linux container, so the page separates two kinds of code. The XAML
files and the code-behind of the Shelf WPF application (the project in the next section) were built, not run: they compiled with
warnings treated as errors on the .NET 10 SDK with EnableWindowsTargeting on, but no window was ever shown. The view models,
commands and the CommunityToolkit.Mvvm classes do not touch System.Windows, so they live in an ordinary net10.0 class library, and
those examples were built and run as console programs with the real output shown. That split is also the recommended architecture:
the part of a WPF application that holds the logic should be testable on any machine.
Windows Forms is covered on Windows Forms Applications; the two frameworks share the Windows desktop runtime and the Visual Basic support code, but WPF has its own template, its own markup language and a very different data model. The reference scenario is the shelf library catalogue; the WPF front is a window that lists the books of the catalogue, lets the user add and remove them, and shows a status line.
What WPF is, and what Visual Basic gets
WPF (Windows Presentation Foundation) draws every control itself on top of DirectX, describes the user interface in XAML (an XML dialect) and
connects it to code through data binding, commands, styles and templates. It shipped with .NET Framework 3.0 in 2006 and was ported to .NET
Core 3.0, so it runs on .NET 10, but only on Windows: the target framework is net10.0-windows and the project sets UseWPF. Visual Basic is
a supported language for it. dotnet new has the Visual Basic templates wpf, wpflib, wpfcustomcontrollib and wpfusercontrollib
(since .NET 5). What Visual Basic does not get is anything that depends on C# tooling: the C# attributes of the MVVM Toolkit (see below) generate
nothing for it, and the framework itself does not support trimming or Native AOT for any language.
| Question | Answer for Visual Basic on .NET 10 |
|---|---|
Which template? |
|
Which target framework? |
|
Runs on Linux or macOS? |
No. It builds there when |
Cross-platform replacement? |
None in Visual Basic: .NET MAUI has no Visual Basic support. See Visual Basic and .NET for the support matrix. |
Browser hosting (XBAP)? |
Gone. Older books that show a WPF application running inside Internet Explorer describe a technology that .NET does not have. |
Single-file, trimmed or AOT? |
Framework-dependent and self-contained publishing work; trimming and Native AOT are not supported for WPF applications. |
The template and the project file
The template creates a project file, an Application class, a main window and an assembly-level attribute. The project file that
dotnet new wpf -lang VB wrote on the .NET 10 SDK is:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<RootNamespace>Tpl</RootNamespace>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<Import Include="System.Windows" />
<Import Include="System.Windows.Controls" />
<Import Include="System.Windows.Data" />
<Import Include="System.Windows.Documents" />
<Import Include="System.Windows.Input" />
<Import Include="System.Windows.Media" />
<Import Include="System.Windows.Media.Imaging" />
<Import Include="System.Windows.Navigation" />
<Import Include="System.Windows.Shapes" />
</ItemGroup>
</Project>
Four details matter for Visual Basic. OutputType is WinExe, so no console window opens. UseWPF adds the WPF reference assemblies and the
XAML build tasks. The Import items are project-level imports: every file of the project behaves as if it started with those Imports
lines, which is why the template’s code-behind can use RoutedEventArgs without any Imports statement. And, as with every SDK project, none
of the Option statements is set: the template’s code-behind compiles with Option Strict Off, which is the first thing to change (see
Option Statements). The examples on this page keep the project property
OptionStrict at On (through a shared Directory.Build.props) and also write Option Strict On and the explicit Imports System.Windows
at the top of each file, so they compile whether or not the project-level imports exist.
The template files are:
| File | Purpose |
|---|---|
|
The application object: |
|
|
|
The window’s markup: |
|
|
|
The |
|
The project file above. |
Source: Create a WPF app and Application management overview
XAML and code-behind: x:Class leaves out the root namespace
A XAML file and its code-behind are two halves of one class. The XAML compiler (the "markup compile" step of the build) reads
MainWindow.xaml and writes a partial class MainWindow into obj/Debug/net10.0-windows/MainWindow.g.vb; your file MainWindow.xaml.vb
holds the other half. The two meet through the name in x:Class, and here Visual Basic differs from C# in a way that costs many beginners an hour:
-
In C#,
x:Classis the full name of the class, namespace included:x:Class="Shelf.Wpf.MainWindow". -
In Visual Basic,
x:Classis the name relative to the project’s root namespace. WithRootNamespaceset toShelf.Wpf, the window’s full name isShelf.Wpf.MainWindowand the attribute saysx:Class="MainWindow".
If you write the namespace out in a Visual Basic project, the generated half becomes Shelf.Wpf.Shelf.Wpf.MainWindow (the root namespace is added in
front of the name you gave), your code-behind no longer joins it, and the build reports errors in the code-behind, not in the XAML. Changing
x:Class in the example project to Shelf.Wpf.MainWindow produced:
MainWindow.xaml.vb(9,86): error BC30590: Event 'Loaded' cannot be found.
MainWindow.xaml.vb(13,110): error BC30506: Handles clause requires a WithEvents variable defined in the containing type or one of its base types.
MainWindow.xaml.vb(21,90): error BC30506: Handles clause requires a WithEvents variable defined in the containing type or one of its base types.
The opposite mistake does not exist in the same form for namespaces in xmlns declarations: there the Visual Basic rule is the C# rule. A CLR
namespace in XAML is written with its real, full name, so a helper class in the same project is referenced as
xmlns:local="clr-namespace:Shelf.Wpf" (the template writes exactly that, using the root namespace), and a class from another assembly adds
;assembly=Shelf.ViewModels. Only x:Class is relative.
The generated half is worth reading once. This is the relevant part of the MainWindow.g.vb that the build wrote for the window below, with
the long Imports list, the Option Strict Off line (see below) and the #ExternalSource markers removed:
Option Explicit On
<Microsoft.VisualBasic.CompilerServices.DesignerGenerated()>
Partial Public Class MainWindow
Inherits System.Windows.Window
Implements System.Windows.Markup.IComponentConnector
Friend WithEvents IsbnBox As System.Windows.Controls.TextBox
Friend WithEvents LookupButton As System.Windows.Controls.Button
Friend WithEvents LookupStatus As System.Windows.Controls.TextBlock
Friend WithEvents RemoveButton As System.Windows.Controls.Button
Friend WithEvents AboutButton As System.Windows.Controls.Button
Friend WithEvents BookList As System.Windows.Controls.ListBox
Private _contentLoaded As Boolean
Public Sub InitializeComponent() Implements System.Windows.Markup.IComponentConnector.InitializeComponent
If _contentLoaded Then
Return
End If
_contentLoaded = true
Dim resourceLocater As System.Uri = New System.Uri("/Shelf.Wpf;component/mainwindow.xaml", System.UriKind.Relative)
System.Windows.Application.LoadComponent(Me, resourceLocater)
End Sub
Sub System_Windows_Markup_IComponentConnector_Connect(ByVal connectionId As Integer, ByVal target As Object) Implements System.Windows.Markup.IComponentConnector.Connect
If (connectionId = 1) Then
Me.IsbnBox = CType(target,System.Windows.Controls.TextBox)
Return
End If
If (connectionId = 2) Then
Me.LookupButton = CType(target,System.Windows.Controls.Button)
Return
End If
If (connectionId = 3) Then
Me.LookupStatus = CType(target,System.Windows.Controls.TextBlock)
Return
End If
If (connectionId = 4) Then
Me.RemoveButton = CType(target,System.Windows.Controls.Button)
Return
End If
If (connectionId = 5) Then
Me.AboutButton = CType(target,System.Windows.Controls.Button)
AddHandler Me.AboutButton.Click, New System.Windows.RoutedEventHandler(AddressOf Me.AboutButton_Click)
Return
End If
If (connectionId = 6) Then
Me.BookList = CType(target,System.Windows.Controls.ListBox)
Return
End If
Me._contentLoaded = true
End Sub
End Class
Three facts follow from this listing. First, the generated file is Option Strict Off, but that does not weaken your code: Option Strict is a
per-file setting, and your half keeps its own. Second, every element that has an x:Name becomes a Friend WithEvents field, which is exactly what
a Handles clause needs. Third, an event written as an attribute in XAML (Click="AboutButton_Click") is turned into an AddHandler statement in
Connect; the target method must exist in the class, or the generated file fails to compile.
Events: Handles or XAML attributes
Visual Basic gives a WPF window two ways to attach a handler, and a project can mix them.
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Shelf.Wpf"
xmlns:vm="clr-namespace:Shelf.ViewModels;assembly=Shelf.ViewModels"
Title="Shelf" Height="360" Width="480">
<Window.Resources>
<local:BooleanToVisibilityConverter x:Key="BoolToVisibility" />
<DataTemplate DataType="{x:Type vm:BookViewModel}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Display}" Margin="0,0,8,0" />
<TextBlock Text="{Binding Isbn}" Opacity="0.6" />
</StackPanel>
</DataTemplate>
</Window.Resources>
<Window.DataContext>
<vm:MainViewModel />
</Window.DataContext>
<DockPanel Margin="8">
<TextBlock DockPanel.Dock="Top" Style="{StaticResource Heading}" Text="Shelf" />
<Grid DockPanel.Dock="Top" Margin="0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding NewTitle, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Grid.Column="1" Text="{Binding NewIsbn, UpdateSourceTrigger=PropertyChanged}" Margin="4,0" />
<Button Grid.Column="2" Content="Add" Command="{Binding AddCommand}" />
</Grid>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Margin="0,4,0,0">
<TextBox x:Name="IsbnBox" Width="140" />
<Button x:Name="LookupButton" Content="Look up" />
<TextBlock x:Name="LookupStatus" VerticalAlignment="Center" Margin="8,0" />
</StackPanel>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal">
<Button x:Name="RemoveButton" Content="Remove" Command="{Binding RemoveCommand}" />
<Button x:Name="AboutButton" Content="About" Click="AboutButton_Click" />
<TextBlock Text="{Binding Status}" VerticalAlignment="Center" Margin="8,0" />
</StackPanel>
<ListBox x:Name="BookList" ItemsSource="{Binding Books}" SelectedItem="{Binding Selected}" />
</DockPanel>
</Window>
Option Strict On
Imports System.Net.Http
Imports System.Windows
Class MainWindow
Private ReadOnly _lookup As New IsbnLookup(New HttpClient())
Private Sub MainWindow_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
Title = "Shelf - " & BookList.Items.Count & " book(s)"
End Sub
Private Sub BookList_SelectionChanged(sender As Object, e As Controls.SelectionChangedEventArgs) Handles BookList.SelectionChanged
RemoveButton.IsEnabled = BookList.SelectedItem IsNot Nothing
End Sub
Private Sub AboutButton_Click(sender As Object, e As RoutedEventArgs)
MessageBox.Show("Shelf, a catalogue.", "About")
End Sub
Private Async Sub LookupButton_Click(sender As Object, e As RoutedEventArgs) Handles LookupButton.Click
LookupButton.IsEnabled = False
Dim failure As String = Nothing
Try
LookupStatus.Text = Await _lookup.FindTitleAsync(IsbnBox.Text) ' resumes on the UI thread
Catch ex As HttpRequestException
failure = ex.Message
Finally
LookupButton.IsEnabled = True
End Try
If failure IsNot Nothing Then LookupStatus.Text = "Lookup failed: " & failure
End Sub
End Class
The first handler uses Handles Me.Loaded: Loaded is an event of the window itself, so no x:Name is needed. The second uses
Handles BookList.SelectionChanged, which works because BookList has an x:Name and therefore a WithEvents field. The third is wired
in the XAML with Click="AboutButton_Click" and has no Handles clause. The fourth is an Async Sub with Handles LookupButton.Click, explained in the threading section below. Both styles end up as a normal event subscription; the difference is
where the wiring is visible. Handles keeps it in code, can name several events (Handles A.Click, B.Click), and is checked by the compiler against
the handler’s signature; the attribute keeps it next to the element.
Do not write both for the same method and event: the generated code subscribes once for the attribute and the WithEvents machinery subscribes
once for Handles, so the handler would run twice on every click.
Errors that the example project produced when the wiring was broken:
| Mistake | Compiler message | Why |
|---|---|---|
|
BC30590 |
The XAML half and the code-behind half are different classes. |
|
BC30456 |
The generated |
|
BC30451 |
Only named elements become fields. |
|
BC36663 |
The handler parameter must be the event’s argument type or a base of it. |
|
BC30506 |
Misspelled names and elements without |
No |
BC30002 |
|
Source: Handles clause and Code-behind and XAML in WPF
The same pair of mechanisms is explained for plain .NET events on Events and Handlers.
Event handlers that await work are Async Sub, the one place where Async Sub is the right choice; see
Threads, the Dispatcher and async handlers below and Async and Await.
The Application class
Application.xaml declares the application object and Application.xaml.vb is its code-behind. In the template there is no Sub Main in your code:
the XAML build step generates an entry point that creates the Application, calls InitializeComponent and Run, and StartupUri names the XAML
file that becomes the first window. The shelf application adds a merged resource dictionary and handlers for the two application events that
nearly every program needs:
<Application x:Class="Application"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml"
Startup="Application_Startup"
DispatcherUnhandledException="Application_DispatcherUnhandledException">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
Option Strict On
Imports System.Windows
Imports System.Windows.Threading
Class Application
Private Sub Application_Startup(sender As Object, e As StartupEventArgs)
Debug.WriteLine("Shelf started with " & e.Args.Length & " argument(s)")
End Sub
Private Sub Application_DispatcherUnhandledException(sender As Object, e As DispatcherUnhandledExceptionEventArgs)
MessageBox.Show(e.Exception.Message, "Shelf", MessageBoxButton.OK, MessageBoxImage.Error)
e.Handled = True
End Sub
End Class
| Event or member | Use |
|---|---|
|
Runs before the first window. |
|
Runs when the application shuts down; |
|
Raised for an exception that escaped a handler on the UI thread. Setting |
|
|
|
The running |
|
In .NET 9 and later, |
If you need your own entry point (to parse arguments before any window is created, or to enforce a single instance with a named Mutex), do what the
Windows Forms page does: set <StartupObject> to a module with Sub Main, mark it <STAThread>, create New Application() yourself, and call
Run(New MainWindow()); remember to remove StartupUri or the first window will be created twice. Visual Basic’s WindowsFormsApplicationBase
single-instance feature belongs to Windows Forms, not WPF.
Data binding
A binding connects a property of a UI element (the target) to a property of any .NET object (the source). The source is found through the element’s
DataContext, which is inherited down the element tree: set it once on the window and every descendant binds against it. Bindings are written in
markup extensions: {Binding Title}, {Binding Path=Title, Mode=TwoWay}, {StaticResource Heading}. Everything inside the braces is parsed at run
time by reflection on the source’s public properties, which has consequences for Visual Basic:
-
A binding to a field does nothing. Visual Basic makes it easy to write
Public Title As String; WPF binds only to properties. -
A misspelled property name is not a compile error. The binding silently shows nothing, and the only trace is a message in the debugger’s Output window (the
System.Windows.Datatrace source). Cover the view model with unit tests, and watch the Output window while exercising the window. -
A binding updates the UI when the source raises
PropertyChanged(or the property is a dependency property) and updates the source when the target changes, depending onMode(OneWay,TwoWay,OneTime,OneWayToSource) andUpdateSourceTrigger(PropertyChanged,LostFocus,Explicit). ATextBox.Textbinding isTwoWayby default and updates the source when the box loses focus, so writeUpdateSourceTrigger=PropertyChangedwhere a button must enable while the user types.
INotifyPropertyChanged in Visual Basic
The change notification is the INotifyPropertyChanged interface. The base class used by the shelf view models implements it once; SetField compares,
assigns and raises, and the CallerMemberName attribute fills in the property name so that no string literal is needed:
Option Strict On
Imports System.ComponentModel
Imports System.Runtime.CompilerServices
Public MustInherit Class ViewModelBase
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Protected Function SetField(Of T)(ByRef field As T, value As T, <CallerMemberName> Optional propertyName As String = Nothing) As Boolean
If EqualityComparer(Of T).Default.Equals(field, value) Then
Return False
End If
field = value
OnPropertyChanged(propertyName)
Return True
End Function
Protected Sub OnPropertyChanged(<CallerMemberName> Optional propertyName As String = Nothing)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
End Class
Visual Basic specifics: the event is declared with Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
(the Implements clause, not C#'s implicit match); the attribute on an optional parameter is <CallerMemberName> Optional propertyName As String = Nothing;
the property name for a related property is NameOf(Display); and and the ByRef field As T parameter receives the private backing field.
Collections need the equivalent interface, INotifyCollectionChanged, which ObservableCollection(Of T) implements. A List(Of T) bound to an ItemsControl shows
its contents once and never updates; an ObservableCollection(Of T) raises CollectionChanged for Add, Remove, Move and Clear. Changing the item is a different
matter: the item must itself raise PropertyChanged, which is why the view model below wraps each book in a BookViewModel.
MVVM and commands
Model-View-ViewModel is the structure that WPF’s binding system was designed for: the view is XAML with almost no code, the view model exposes properties and
commands, and the model is the domain (here Shelf.Core). A command is an object that implements System.Windows.Input.ICommand: Execute(parameter), CanExecute(parameter) and the
CanExecuteChanged event. Buttons bind their Command property to it, and a button whose command says CanExecute = False disables itself.
The hand-written command and the view model are plain net10.0 code. The command wraps two delegates; passing AddressOf for a method that has a parameter As Object
argument works, and a Function(p) ... lambda supplies the optional predicate:
Option Strict On
Imports System.Windows.Input
Public Class DelegateCommand
Implements ICommand
Private ReadOnly _execute As Action(Of Object)
Private ReadOnly _canExecute As Func(Of Object, Boolean)
Public Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged
Public Sub New(execute As Action(Of Object), Optional canExecute As Func(Of Object, Boolean) = Nothing)
If execute Is Nothing Then Throw New ArgumentNullException(NameOf(execute))
_execute = execute
_canExecute = canExecute
End Sub
Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute
Return _canExecute Is Nothing OrElse _canExecute(parameter)
End Function
Public Sub Execute(parameter As Object) Implements ICommand.Execute
_execute(parameter)
End Sub
Public Sub RaiseCanExecuteChanged()
RaiseEvent CanExecuteChanged(Me, EventArgs.Empty)
End Sub
End Class
Option Strict On
Imports System.Collections.ObjectModel
Imports Shelf.Core
Public Class BookViewModel
Inherits ViewModelBase
Private _title As String
Private _year As Integer
Public Sub New(title As String, year As Integer, isbn As String)
_title = title
_year = year
Me.Isbn = isbn
End Sub
Public Property Title As String
Get
Return _title
End Get
Set(value As String)
SetField(_title, value)
End Set
End Property
Public Property Year As Integer
Get
Return _year
End Get
Set(value As Integer)
If SetField(_year, value) Then OnPropertyChanged(NameOf(Display))
End Set
End Property
Public ReadOnly Property Isbn As String
Public ReadOnly Property Display As String
Get
Return $"{_title} ({_year})"
End Get
End Property
End Class
Public Class MainViewModel
Inherits ViewModelBase
Private _newTitle As String = ""
Private _newIsbn As String = ""
Private _selected As BookViewModel
Private _status As String = "Ready"
Private Const DefaultYear As Integer = 2026
Public ReadOnly Property Books As New ObservableCollection(Of BookViewModel)
Public ReadOnly Property AddCommand As DelegateCommand
Public ReadOnly Property RemoveCommand As DelegateCommand
Public Sub New()
AddCommand = New DelegateCommand(AddressOf AddBook, AddressOf CanAdd)
RemoveCommand = New DelegateCommand(AddressOf RemoveSelected, Function(p) Selected IsNot Nothing)
End Sub
Public Property NewTitle As String
Get
Return _newTitle
End Get
Set(value As String)
If SetField(_newTitle, value) Then AddCommand.RaiseCanExecuteChanged()
End Set
End Property
Public Property NewIsbn As String
Get
Return _newIsbn
End Get
Set(value As String)
If SetField(_newIsbn, value) Then AddCommand.RaiseCanExecuteChanged()
End Set
End Property
Public Property Selected As BookViewModel
Get
Return _selected
End Get
Set(value As BookViewModel)
If SetField(_selected, value) Then RemoveCommand.RaiseCanExecuteChanged()
End Set
End Property
Public Property Status As String
Get
Return _status
End Get
Private Set(value As String)
SetField(_status, value)
End Set
End Property
Private Function CanAdd(parameter As Object) As Boolean
Dim parsed As Isbn = Nothing
Return Not String.IsNullOrWhiteSpace(NewTitle) AndAlso Isbn.TryParse(NewIsbn, parsed)
End Function
Private Sub AddBook(parameter As Object)
Books.Add(New BookViewModel(NewTitle, DefaultYear, Isbn.Parse(NewIsbn).Value))
Status = $"Added '{NewTitle}'"
NewTitle = ""
NewIsbn = ""
End Sub
Private Sub RemoveSelected(parameter As Object)
Dim removed = Selected
Books.Remove(removed)
Selected = Nothing
Status = $"Removed '{removed.Title}'"
End Sub
End Class
The XAML shown earlier binds Text="{Binding NewTitle, UpdateSourceTrigger=PropertyChanged}", Command="{Binding AddCommand}", ItemsSource="{Binding Books}" and
SelectedItem="{Binding Selected}" against this class, with <vm:MainViewModel /> as the window’s DataContext. Because the view model has no reference to System.Windows,
a console program can drive it exactly as the UI would, and this one was built and run (the root namespace of the demo is VmDemo):
Option Strict On
Imports System.Collections.Specialized
Imports System.ComponentModel
Imports Shelf.ViewModels
Module Program
Sub Main()
Dim vm As New MainViewModel()
AddHandler vm.PropertyChanged, Sub(s, e) Console.WriteLine("PropertyChanged: " & e.PropertyName)
AddHandler vm.Books.CollectionChanged, Sub(s, e) Console.WriteLine("CollectionChanged: " & e.Action.ToString())
AddHandler vm.AddCommand.CanExecuteChanged, Sub(s, e) Console.WriteLine("AddCommand.CanExecuteChanged")
Console.WriteLine("CanExecute (empty): " & vm.AddCommand.CanExecute(Nothing))
vm.NewTitle = "Dune"
vm.NewIsbn = "978-0-441-17271-9"
Console.WriteLine("CanExecute (filled): " & vm.AddCommand.CanExecute(Nothing))
vm.AddCommand.Execute(Nothing)
Console.WriteLine("Books: " & vm.Books.Count & ", status: " & vm.Status)
vm.Selected = vm.Books(0)
Console.WriteLine("Remove enabled: " & vm.RemoveCommand.CanExecute(Nothing))
vm.Books(0).Year = 1966
Console.WriteLine("Display: " & vm.Books(0).Display)
vm.RemoveCommand.Execute(Nothing)
Console.WriteLine("Books: " & vm.Books.Count & ", status: " & vm.Status)
End Sub
End Module
CanExecute (empty): False
PropertyChanged: NewTitle
AddCommand.CanExecuteChanged
PropertyChanged: NewIsbn
AddCommand.CanExecuteChanged
CanExecute (filled): True
CollectionChanged: Add
PropertyChanged: Status
PropertyChanged: NewTitle
AddCommand.CanExecuteChanged
PropertyChanged: NewIsbn
AddCommand.CanExecuteChanged
Books: 1, status: Added 'Dune'
PropertyChanged: Selected
Remove enabled: True
Display: Dune (1966)
CollectionChanged: Remove
PropertyChanged: Selected
PropertyChanged: Status
Books: 0, status: Removed 'Dune'
Source: Commanding overview and ICommand
Read the output as the contract between view model and view: setting NewTitle and NewIsbn raises PropertyChanged and CanExecuteChanged so the Add button
enables; Execute adds to the collection (CollectionChanged: Add) and clears the input fields, which disables the button again; and the Display property of an item
is refreshed when its Year changes. In production code, move the unit tests of such a class to the same pattern; see Testing.
Two mistakes the example project made along the way are typical. The first view model class was named RelayCommand, the same as the Toolkit class used below. A type declared in your own namespace
wins over an imported one with the same name, so the toolkit code failed with BC36663 and BC30456 ('NotifyCanExecuteChanged' is not a member of 'RelayCommand'), pointing at the toolkit’s members of a class that was
not the toolkit’s. Give your own commands a different name (DelegateCommand) or qualify the toolkit’s. The second mistake was setting a project’s root namespace to Shelf.ViewModels and wrapping the
files in Namespace Shelf.ViewModels, which produced Shelf.ViewModels.Shelf.ViewModels (BC30002 Type 'MainViewModel' is not defined in the consumer). The root namespace is already added to every Namespace block.
CommunityToolkit.Mvvm without source generators
CommunityToolkit.Mvvm (the MVVM Toolkit, package CommunityToolkit.Mvvm, version 8.4.0 here) is the Microsoft-maintained library that replaces the hand-written plumbing above. It has two halves:
runtime classes (ObservableObject, ObservableRecipient, RelayCommand, AsyncRelayCommand, WeakReferenceMessenger) and Roslyn source generators that read attributes such as
[ObservableProperty] and [RelayCommand] and write the properties and commands for you. Roslyn source generators are language-specific, and the Toolkit’s generators are written for C#.
The runtime classes work from Visual Basic; the attributes compile but generate nothing.
Option Strict On
Imports CommunityToolkit.Mvvm.ComponentModel
Imports CommunityToolkit.Mvvm.Input
' ObservableObject, SetProperty and the command classes work in Visual Basic.
' The [ObservableProperty] and [RelayCommand] attributes need a C# source generator, so they are not used.
Public Class LoanViewModel
Inherits ObservableObject
Private _borrower As String = ""
Private _daysLeft As Integer = 14
Public Sub New()
ExtendCommand = New RelayCommand(AddressOf Extend, Function() DaysLeft < 28)
LookupCommand = New AsyncRelayCommand(AddressOf LookupAsync)
End Sub
Public Property Borrower As String
Get
Return _borrower
End Get
Set(value As String)
SetProperty(_borrower, value)
End Set
End Property
Public Property DaysLeft As Integer
Get
Return _daysLeft
End Get
Set(value As Integer)
If SetProperty(_daysLeft, value) Then ExtendCommand.NotifyCanExecuteChanged()
End Set
End Property
Public ReadOnly Property ExtendCommand As RelayCommand
Public ReadOnly Property LookupCommand As AsyncRelayCommand
Private Sub Extend()
DaysLeft += 7
End Sub
Private Async Function LookupAsync() As Task
Await Task.Delay(10)
Borrower = "Ada Lovelace"
End Function
End Class
Option Strict On
Imports Shelf.ViewModels
Module Program
Sub Main()
RunAsync().GetAwaiter().GetResult()
End Sub
Private Async Function RunAsync() As Task
Dim loan As New LoanViewModel()
AddHandler loan.PropertyChanged, Sub(s, e) Console.WriteLine("PropertyChanged: " & e.PropertyName)
AddHandler loan.ExtendCommand.CanExecuteChanged, Sub(s, e) Console.WriteLine("ExtendCommand.CanExecuteChanged")
For i = 1 To 3
Console.WriteLine($"Extend #{i} allowed: {loan.ExtendCommand.CanExecute(Nothing)}")
If loan.ExtendCommand.CanExecute(Nothing) Then loan.ExtendCommand.Execute(Nothing)
Next
Console.WriteLine("Days left: " & loan.DaysLeft)
Await loan.LookupCommand.ExecuteAsync(Nothing)
Console.WriteLine("Borrower: " & loan.Borrower)
End Function
End Module
Extend #1 allowed: True
PropertyChanged: DaysLeft
ExtendCommand.CanExecuteChanged
Extend #2 allowed: True
PropertyChanged: DaysLeft
ExtendCommand.CanExecuteChanged
Extend #3 allowed: False
Days left: 28
PropertyChanged: Borrower
Borrower: Ada Lovelace
The messenger and the validator work as well. This second program (also built and run) validates a data-annotated property and sends a message between two objects:
Option Strict On
Imports System.ComponentModel.DataAnnotations
Imports CommunityToolkit.Mvvm.ComponentModel
Imports CommunityToolkit.Mvvm.Messaging
Imports CommunityToolkit.Mvvm.Messaging.Messages
Public Class FormModel
Inherits ObservableValidator
Private _title As String = "Dune"
<Required(ErrorMessage:="Title is required")>
Public Property Title As String
Get
Return _title
End Get
Set(value As String)
SetProperty(_title, value, validate:=True)
End Set
End Property
End Class
Module More
Sub Main()
Dim form As New FormModel()
form.Title = ""
Console.WriteLine("HasErrors: " & form.HasErrors & ", first: " & form.GetErrors(NameOf(FormModel.Title)).First().ErrorMessage)
Dim recipient As New Object()
WeakReferenceMessenger.Default.Register(Of ValueChangedMessage(Of String))(recipient,
Sub(r, m) Console.WriteLine("Received: " & m.Value))
WeakReferenceMessenger.Default.Send(New ValueChangedMessage(Of String)("Loan extended"))
GC.KeepAlive(recipient)
End Sub
End Module
HasErrors: True, first: Title is required
Received: Loan extended
The base class, SetProperty, RelayCommand with a CanExecute predicate, NotifyCanExecuteChanged and AsyncRelayCommand (which exposes ExecuteAsync, IsRunning and a cancellation
command) all behave as documented. The attribute route, by contrast, compiles but produces no property, so the first use of the "generated" member fails:
Option Strict On
Imports CommunityToolkit.Mvvm.ComponentModel
Public Class BookModel
Inherits ObservableObject
<ObservableProperty>
Private _title As String = "Dune"
End Class
Module Program
Sub Main()
Dim model As New BookModel()
Console.WriteLine(model.Title)
End Sub
End Module
GenFail/Program.vb(15,27): error BC30456: 'Title' is not a member of 'BookModel'.
| Toolkit feature | Works in Visual Basic? | What to write instead |
|---|---|---|
|
Yes |
Write properties by hand as above. |
|
Yes |
|
|
No (compiles, generates nothing) |
A hand-written property that calls |
|
No |
A |
|
No |
Call |
|
Yes |
Used exactly as in C#. |
|
Yes (data annotations on properties) |
Properties and |
Source: MVVM Toolkit, ObservableObject, RelayCommand and MVVM source generators
Styles, templates and resources
Look and behaviour are separated from structure by resources. A ResourceDictionary maps keys to brushes, styles, templates and converters; elements look keys up
with {StaticResource Key} (resolved once at load) or {DynamicResource Key} (re-resolved when the resource changes), walking from the element up through the window’s resources
and the application’s. The application’s dictionary is where the shelf styles live; this file is merged by Application.xaml:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="AccentBrush" Color="#2B6CB0" />
<Style TargetType="Button">
<Setter Property="Margin" Value="4" />
<Setter Property="Padding" Value="12,4" />
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.5" />
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="Heading" TargetType="TextBlock">
<Setter Property="FontSize" Value="18" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{StaticResource AccentBrush}" />
</Style>
</ResourceDictionary>
The implicit Style TargetType="Button" (no x:Key) applies to every button in the application; the keyed style Heading applies only where an element asks for it with
Style="{StaticResource Heading}", as the window does. A DataTemplate with DataType="{x:Type vm:BookViewModel}" (in MainWindow.xaml) tells WPF how to draw any BookViewModel it meets,
which is how the list box shows the title and ISBN without an ItemTemplate. A ControlTemplate replaces a control’s whole visual tree and is the tool for restyling; none of
this changes between C# and Visual Basic, because it is XAML.
What is different in Visual Basic is the code that XAML refers to. A value converter is a class that implements IValueConverter, and the interface members are implemented with Implements:
Option Strict On
Imports System.Globalization
Imports System.Windows
Imports System.Windows.Data
Public Class BooleanToVisibilityConverter
Implements IValueConverter
Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert
Return If(TypeOf value Is Boolean AndAlso DirectCast(value, Boolean), Visibility.Visible, Visibility.Collapsed)
End Function
Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
Throw New NotSupportedException()
End Function
End Class
The XAML creates one in the resources (<local:BooleanToVisibilityConverter x:Key="BoolToVisibility" />) and uses it as {Binding IsBusy, Converter={StaticResource BoolToVisibility}}. The converter
class must be public, with a public parameterless constructor, and in the namespace named by the xmlns:local declaration, which is the full root-namespace-qualified name. Note that
Convert is also the name of the System.Convert class: inside a class that has a Convert member the member wins over the type, so Convert.ToInt32(value) there fails with BC30455 Argument not specified for parameter 'value' of 'Public Function Convert(value As Object) As Object'. and BC30574 Option Strict On disallows late binding.; write System.Convert.ToInt32(value).
Dependency properties, routed events and attached properties in Visual Basic
Properties that take part in binding, styling, animation and inheritance are dependency properties: the value lives in the property store of the DependencyObject rather than in a field, and
a registered DependencyProperty identifies it. A custom control exposes one with three pieces: the static identifier, the Register call, and a CLR wrapper. Written in Visual Basic, with a
coerce callback, a change callback, a routed event and an attached property:
Option Strict On
Imports System.Windows
Imports System.Windows.Controls
Public Class RatingBar
Inherits Control
Public Shared ReadOnly RatingProperty As DependencyProperty =
DependencyProperty.Register(NameOf(Rating), GetType(Integer), GetType(RatingBar),
New FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
AddressOf OnRatingChanged, AddressOf CoerceRating))
Public Shared ReadOnly RatingChangedEvent As RoutedEvent =
EventManager.RegisterRoutedEvent(NameOf(RatingChanged), RoutingStrategy.Bubble,
GetType(RoutedPropertyChangedEventHandler(Of Integer)), GetType(RatingBar))
Public Shared ReadOnly IsFavoriteProperty As DependencyProperty =
DependencyProperty.RegisterAttached("IsFavorite", GetType(Boolean), GetType(RatingBar), New PropertyMetadata(False))
Public Property Rating As Integer
Get
Return CInt(GetValue(RatingProperty))
End Get
Set(value As Integer)
SetValue(RatingProperty, value)
End Set
End Property
Public Custom Event RatingChanged As RoutedPropertyChangedEventHandler(Of Integer)
AddHandler(value As RoutedPropertyChangedEventHandler(Of Integer))
[AddHandler](RatingChangedEvent, value)
End AddHandler
RemoveHandler(value As RoutedPropertyChangedEventHandler(Of Integer))
[RemoveHandler](RatingChangedEvent, value)
End RemoveHandler
RaiseEvent(sender As Object, e As RoutedPropertyChangedEventArgs(Of Integer))
[RaiseEvent](e)
End RaiseEvent
End Event
Public Shared Function GetIsFavorite(element As DependencyObject) As Boolean
Return CBool(element.GetValue(IsFavoriteProperty))
End Function
Public Shared Sub SetIsFavorite(element As DependencyObject, value As Boolean)
element.SetValue(IsFavoriteProperty, value)
End Sub
Private Shared Function CoerceRating(d As DependencyObject, baseValue As Object) As Object
Return Math.Clamp(CInt(baseValue), 0, 5)
End Function
Private Shared Sub OnRatingChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
Dim bar = DirectCast(d, RatingBar)
bar.RaiseEvent(New RoutedPropertyChangedEventArgs(Of Integer)(CInt(e.OldValue), CInt(e.NewValue), RatingChangedEvent))
End Sub
End Class
The Visual Basic rules, each of which has a way to go wrong silently:
-
The identifier is
Public Shared ReadOnly RatingProperty As DependencyPropertyand, by convention and by the framework’s name lookup, its name is the wrapper’s name plusProperty. The wrapper propertyRatingcallsGetValueandSetValueand nothing else: the XAML parser and the binding engine bypass the wrapper and set the dependency property directly, so any other code inSetwould not run. Put side effects in the change callback. -
GetValuereturnsObject, so underOption Strict Onevery read needs a conversion:CInt(GetValue(RatingProperty)).CIntunboxes a boxedIntegerand would also accept a boxedDouble;DirectCastis stricter and fails withInvalidCastExceptionif the registered type and the declared type disagree. -
The default value passed to
PropertyMetadatais typedObject, so the compiler cannot check it against the registered type. Pass a value of exactly that type (0for anIntegerproperty,0.0for aDoubleone); a mismatch is reported when the class loads, not when the project is built. -
Callbacks are
Sharedmethods and are passed withAddressOf. Because Visual Basic identifiers are case-insensitive, a callback namedOnRatingChangedand a fieldRatingChanged...must differ in more than case; the example’s event identifier isRatingChangedEventand the CLR event isRatingChanged. -
A routed event’s CLR wrapper is a
Custom EventwithAddHandler,RemoveHandlerandRaiseEventblocks that forward to theUIElementmethods, whose names are bracketed ([AddHandler],[RemoveHandler],[RaiseEvent]) because they are also Visual Basic keywords. -
An attached property uses
RegisterAttachedand a pair ofSharedmethods namedGetXandSetXtaking theDependencyObject. The XAMLlocal:RatingBar.IsFavorite="True"is resolved through those methods. -
Static field initialisers of a class run in its shared constructor, so the order of the
Shared ReadOnlyfields matters when one metadata object refers to another field. Keep each identifier’sRegistercall self-contained.
Threads, the Dispatcher and async handlers
WPF objects belong to the thread that created them (the single UI thread, an STA thread), and every DispatcherObject checks the calling thread. Touching a control from a background thread
throws InvalidOperationException ("The calling thread cannot access this object because a different thread owns it"). The thread’s message queue is the Dispatcher; Dispatcher.Invoke blocks
until the work has run on the UI thread, Dispatcher.BeginInvoke and InvokeAsync do not.
The modern pattern is not to touch the dispatcher at all: an Async Sub event handler (or an AsyncRelayCommand) starts an operation, awaits it, and resumes on the UI thread because WPF installs
a SynchronizationContext that Await captures. This handler is part of the MainWindow.xaml.vb listing shown earlier (with a small IsbnLookup class that wraps an HttpClient) and was built, not run:
Private Async Sub LookupButton_Click(sender As Object, e As RoutedEventArgs) Handles LookupButton.Click
LookupButton.IsEnabled = False
Dim failure As String = Nothing
Try
LookupStatus.Text = Await _lookup.FindTitleAsync(IsbnBox.Text) ' resumes on the UI thread
Catch ex As HttpRequestException
failure = ex.Message
Finally
LookupButton.IsEnabled = True
End Try
If failure IsNot Nothing Then LookupStatus.Text = "Lookup failed: " & failure
End Sub
Option Strict On
Imports System.Net.Http
Public Class IsbnLookup
Private ReadOnly _client As HttpClient
Public Sub New(client As HttpClient)
_client = client
End Sub
Public Async Function FindTitleAsync(isbn As String) As Task(Of String)
Dim json = Await _client.GetStringAsync("https://isbn.example.test/api/" & Uri.EscapeDataString(isbn))
Return json
End Function
End Class
The Visual Basic limits from the async page apply: Await is not allowed inside a Catch or Finally block (BC36943), Async Sub can only be used for event handlers, and a blocking .Result
or .Wait() on the UI thread can deadlock. Do not use ConfigureAwait(False) in code that must update a control afterwards; use it in library code that never touches the UI. Collections bound to the UI
(ObservableCollection) may be changed only on the UI thread; BindingOperations.EnableCollectionSynchronization is the supported exception for collections shared with a background thread.
Source: Threading model and Dispatcher
Building on Linux and macOS, and other platform notes
The project can be built on a non-Windows machine, which is how these examples were checked and how a CI job without Windows runners can still verify that the application compiles. The SDK refuses at first:
Microsoft.NET.Sdk.FrameworkReferenceResolution.targets(120,5): error NETSDK1100: To build a project targeting Windows on this operating system, set the EnableWindowsTargeting property to true.
Setting <EnableWindowsTargeting>true</EnableWindowsTargeting> (in the project or in Directory.Build.props) lets restore download the Windows desktop reference pack and the build succeed. A project that
sets UseWPF without a Windows target framework fails differently:
Microsoft.NET.Sdk.DefaultItems.targets(156,5): error NETSDK1136: The target platform must be set to Windows (usually by including '-windows' in the TargetFramework property) when using Windows Forms or WPF, or referencing projects or packages that do so.
Library projects that the WPF project references can stay on plain net10.0 (the view-model library above); a project that references a net10.0-windows project must itself target Windows.
Because the XAML compiler runs during the build, a build on Linux also validates the XAML syntax and the event-handler names; what it cannot validate is a binding path or a resource key.
| Topic | Note |
|---|---|
Designer |
The XAML designer, Live Visual Tree and XAML Hot Reload are Visual Studio features on Windows. The |
Deployment |
Framework-dependent (needs the .NET Desktop Runtime on the machine), self-contained, single-file, ReadyToRun and MSIX packaging; see Libraries, Packaging and Deployment. |
Testing |
Unit-test view models on any OS. Test windows with UI automation on a Windows runner. |
Interoperability |
WPF and Windows Forms controls can be hosted in each other ( |
XBAP and ClickOnce |
XBAP browser applications do not exist on .NET; ClickOnce still deploys WPF applications. |
WPF in Visual Basic compared with C#
| Topic | Visual Basic | C# |
|---|---|---|
|
Relative to the root namespace: |
Full name: |
Event wiring |
|
XAML attribute or |
Property change notification |
|
|
MVVM Toolkit |
Runtime classes only |
Runtime classes plus |
Dependency property field |
|
|
Routed event wrapper |
|
|
Entry point |
Generated; custom |
Generated; custom |
Project-level imports |
|
Implicit usings or |
Nullable annotations on binding code |
Not applicable |
Nullable reference types add annotations to view models |
Compiled bindings ( |
Not in WPF for either language |
Not in WPF for either language |
Source generators for XAML or binding code |
None for Visual Basic |
Community and Microsoft generators exist |
Where older material is outdated
-
A WPF "overview" written for .NET Framework shows the application running in a browser (XBAP). XBAP is not part of .NET; use
UseWPFand a normal window. -
Older chapters create a WPF project from the "WPF App (.NET Framework)" template with a
StartupUriandApp.xaml. On .NET the files areApplication.xamlandMainWindow.xamland the class isApplication, notApp, in the Visual Basic template (Appis the C# name). -
Older WPF data-binding samples that use
System.Data.SqlClientand designer-generated typed data sets have their replacements on Data Access with ADO.NET and EF Core. -
WPF needs the Windows desktop runtime. It does not run on Linux or macOS; building there is possible, running is not.
Pitfalls
| Pitfall | What to do |
|---|---|
|
Use the name relative to the root namespace. |
Public fields instead of properties for binding |
Bind only to properties; make the setter raise |
|
Add |
Handler wired twice ( |
Use one mechanism per event. |
|
Use |
Side effects in the CLR wrapper of a dependency property |
Move them to the property-changed callback. |
Updating controls from a worker thread |
|
|
Store the exception, leave the block, then await. |
Your own |
Give your class another name. |
Source generator attributes of the Toolkit |
Write the properties and commands by hand. |
Project built on Linux without |
NETSDK1100: set the property. |
|
Turn it on in the project or |
References
-
Windows Presentation Foundation overview, Get started with WPF and Create a WPF app
-
XAML in WPF, XAML syntax in detail and Code-behind and XAML in WPF
-
Data binding in WPF, Data binding overview, Binding declarations overview and Binding sources overview
-
Controls in WPF, Styles and templates and Control styles and templates
-
Dependency property metadata and Dependency property callbacks and validation