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?

dotnet new wpf -lang VB; the library templates are wpflib, wpfcustomcontrollib and wpfusercontrollib.

Which target framework?

net10.0-windows. Plain net10.0 fails with NETSDK1136.

Runs on Linux or macOS?

No. It builds there when EnableWindowsTargeting is true, which is what these examples did.

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

Application.xaml

The application object: x:Class="Application", StartupUri="MainWindow.xaml", the resource dictionary of the whole application.

Application.xaml.vb

Class Application for application-level events (Startup, Exit, DispatcherUnhandledException).

MainWindow.xaml

The window’s markup: x:Class="MainWindow" and an empty Grid.

MainWindow.xaml.vb

Class MainWindow, the code-behind. It is a partial class: the other half is generated from the XAML.

AssemblyInfo.vb

The ThemeInfo assembly attribute, which tells WPF where theme-specific and generic resource dictionaries live.

*.vbproj

The project file above.

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:Class is the full name of the class, namespace included: x:Class="Shelf.Wpf.MainWindow".

  • In Visual Basic, x:Class is the name relative to the project’s root namespace. With RootNamespace set to Shelf.Wpf, the window’s full name is Shelf.Wpf.MainWindow and the attribute says x: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

x:Class="Shelf.Wpf.MainWindow" in a project with root namespace Shelf.Wpf

BC30590 Event 'Loaded' cannot be found. and BC30506 Handles clause requires a WithEvents variable defined in the containing type or one of its base types.

The XAML half and the code-behind half are different classes.

Click="Missing_Click" and no such method

BC30456 'Missing_Click' is not a member of 'MainWindow'. (reported at the XAML line)

The generated AddHandler refers to a method that does not exist.

Handles RemoveButton.Click but the button has no x:Name

BC30451 'RemoveButton' is not declared. It may be inaccessible due to its protection level.

Only named elements become fields.

Handles Me.Loaded on a method whose second parameter is SelectionChangedEventArgs

BC36663 Option Strict On does not allow narrowing in implicit type conversions between method 'Private Sub BookList_SelectionChanged(sender As Object, e As SelectionChangedEventArgs)' and delegate 'Delegate Sub RoutedEventHandler(sender As Object, e As RoutedEventArgs)'.

The handler parameter must be the event’s argument type or a base of it.

Handles Nope.Click where Nope is not a field of the class

BC30506 Handles clause requires a WithEvents variable defined in the containing type or one of its base types.

Misspelled names and elements without x:Name are indistinguishable to the compiler.

No Imports System.Windows (and no project-level import)

BC30002 Type 'RoutedEventArgs' is not defined. followed by BC31029 Method 'MainWindow_Loaded' cannot handle event 'Loaded' because they do not have a compatible signature.

RoutedEventArgs lives in System.Windows; the template relies on its project-level imports.

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

Startup

Runs before the first window. StartupEventArgs.Args carries the command line; use it, or Environment.GetCommandLineArgs(), because a WPF project has no My.Application.CommandLineArgs (the project type does not enable My; MyType is empty for WPF projects).

Exit

Runs when the application shuts down; ExitEventArgs.ApplicationExitCode can be set.

DispatcherUnhandledException

Raised for an exception that escaped a handler on the UI thread. Setting e.Handled = True keeps the application alive. It does not see exceptions on other threads or unobserved task exceptions.

ShutdownMode

OnLastWindowClose (default), OnMainWindowClose or OnExplicitShutdown.

Application.Current

The running Application; Application.Current.Dispatcher is the UI thread’s dispatcher. Cast it to your own type with DirectCast(Application.Current, Application) if you added members.

ThemeMode

In .NET 9 and later, ThemeMode="System" (or Light/Dark) on the Application or a Window selects the Fluent theme. An Application.xaml with ThemeMode="System" compiled in the example project (built, not run); check the current WPF what’s-new page before relying on it, since the feature has been marked experimental in some releases.

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.Data trace 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 on Mode (OneWay, TwoWay, OneTime, OneWayToSource) and UpdateSourceTrigger (PropertyChanged, LostFocus, Explicit). A TextBox.Text binding is TwoWay by default and updates the source when the box loses focus, so write UpdateSourceTrigger=PropertyChanged where 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'

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

ObservableObject, SetProperty, OnPropertyChanged

Yes

Write properties by hand as above.

RelayCommand, AsyncRelayCommand

Yes

New RelayCommand(AddressOf Method, Function() CanDo).

[ObservableProperty] on a field

No (compiles, generates nothing)

A hand-written property that calls SetProperty.

[RelayCommand] on a method

No

A ReadOnly command property created in the constructor.

[NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor]

No

Call OnPropertyChanged or NotifyCanExecuteChanged in the setter.

WeakReferenceMessenger, IMessenger

Yes

Used exactly as in C#.

ObservableValidator

Yes (data annotations on properties)

Properties and ValidateProperty by hand.

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 DependencyProperty and, by convention and by the framework’s name lookup, its name is the wrapper’s name plus Property. The wrapper property Rating calls GetValue and SetValue and nothing else: the XAML parser and the binding engine bypass the wrapper and set the dependency property directly, so any other code in Set would not run. Put side effects in the change callback.

  • GetValue returns Object, so under Option Strict On every read needs a conversion: CInt(GetValue(RatingProperty)). CInt unboxes a boxed Integer and would also accept a boxed Double; DirectCast is stricter and fails with InvalidCastException if the registered type and the declared type disagree.

  • The default value passed to PropertyMetadata is typed Object, so the compiler cannot check it against the registered type. Pass a value of exactly that type (0 for an Integer property, 0.0 for a Double one); a mismatch is reported when the class loads, not when the project is built.

  • Callbacks are Shared methods and are passed with AddressOf. Because Visual Basic identifiers are case-insensitive, a callback named OnRatingChanged and a field RatingChanged... must differ in more than case; the example’s event identifier is RatingChangedEvent and the CLR event is RatingChanged.

  • A routed event’s CLR wrapper is a Custom Event with AddHandler, RemoveHandler and RaiseEvent blocks that forward to the UIElement methods, whose names are bracketed ([AddHandler], [RemoveHandler], [RaiseEvent]) because they are also Visual Basic keywords.

  • An attached property uses RegisterAttached and a pair of Shared methods named GetX and SetX taking the DependencyObject. The XAML local: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 ReadOnly fields matters when one metadata object refers to another field. Keep each identifier’s Register call 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.

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 dotnet CLI and VS Code build and run, but do not design.

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 (WindowsFormsHost, ElementHost) in the same process.

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#

x:Class

Relative to the root namespace: x:Class="MainWindow"

Full name: x:Class="Shelf.Wpf.MainWindow"

Event wiring

Handles Name.Event (needs x:Name) or an XAML attribute

XAML attribute or += in code; no Handles

Property change notification

<CallerMemberName> Optional name As String = Nothing, Implements INotifyPropertyChanged.PropertyChanged

[CallerMemberName] string name = null, implicit interface implementation

MVVM Toolkit

Runtime classes only

Runtime classes plus [ObservableProperty], [RelayCommand] generators

Dependency property field

Public Shared ReadOnly XProperty As DependencyProperty = DependencyProperty.Register(...)

public static readonly DependencyProperty XProperty = ... (and snippet propdp)

Routed event wrapper

Custom Event with [AddHandler]/[RemoveHandler]/[RaiseEvent]

add { AddHandler(...); } remove { ... }

Entry point

Generated; custom Sub Main through StartupObject

Generated; custom Main through StartupObject

Project-level imports

<Import Include="System.Windows" /> items

Implicit usings or using directives

Nullable annotations on binding code

Not applicable

Nullable reference types add annotations to view models

Compiled bindings (x:Bind)

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 UseWPF and a normal window.

  • Older chapters create a WPF project from the "WPF App (.NET Framework)" template with a StartupUri and App.xaml. On .NET the files are Application.xaml and MainWindow.xaml and the class is Application, not App, in the Visual Basic template (App is the C# name).

  • Older WPF data-binding samples that use System.Data.SqlClient and 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

x:Class with the namespace in a VB project

Use the name relative to the root namespace.

Public fields instead of properties for binding

Bind only to properties; make the setter raise PropertyChanged.

Handles on an element without x:Name

Add x:Name, or wire the event in XAML.

Handler wired twice (Handles and Click=)

Use one mechanism per event.

List(Of T) as ItemsSource of a live list

Use ObservableCollection(Of T).

Side effects in the CLR wrapper of a dependency property

Move them to the property-changed callback.

Updating controls from a worker thread

Await back to the UI thread or use Dispatcher.InvokeAsync.

Await in Catch or Finally

Store the exception, leave the block, then await.

Your own RelayCommand next to the Toolkit’s

Give your class another name.

Source generator attributes of the Toolkit

Write the properties and commands by hand.

Project built on Linux without EnableWindowsTargeting

NETSDK1100: set the property.

Option Strict Off in the template

Turn it on in the project or Directory.Build.props.