Migration Triggers & VSM
Key Takeaways
  • WPF property triggers, DataTriggers, MultiTriggers, and EventTriggers all derive from TriggerBase in System.Windows. None of them exist in WinUI 3 or Uno Platform.
  • WinUI 3 retains EventTrigger only for backward compatibility, and only for the Loaded event
  • Microsoft's official migration guidance: replace DataTrigger/MultiTrigger with XAML Behaviors, replace property triggers with VisualStateManager
  • StateTrigger activates a VisualState when its IsActive property is true, giving you a markup-driven replacement for boolean DataTrigger scenarios
  • The VisualStateGroup must be declared under the first child of the root of a control template for state triggers to work

If you are moving a WPF project to Uno Platform or WinUI 3, the word "trigger" will betray you. WinUI keeps the name but drops most of the behavior. This guide maps each of the four WPF trigger flavors to its sanctioned replacement using VisualStateManager, StateTrigger, and XAML Behaviors.

Not The Same

WinUI Triggers Are Not WPF Triggers

WPF's TriggerBase class is the base type for Trigger, MultiTrigger, EventTrigger, DataTrigger, and MultiDataTrigger, all in System.Windows inside PresentationFramework.dll. WinUI 3 and Uno Platform do not ship any of those classes.

The Microsoft.UI.Xaml.EventTrigger class is retained, but it exists mainly for Silverlight compatibility. The only event that invokes it is FrameworkElement.Loaded.

WPF TriggerLives InWinUI 3 / Uno Replacement
Trigger (property trigger)Style.Triggers, ControlTemplate.TriggersVisualState inside a VisualStateGroup
DataTriggerStyle.Triggers, DataTemplate.TriggersStateTrigger with IsActive bound to a VM boolean, or DataTriggerBehavior
MultiTriggerStyle.Triggers, ControlTemplate.TriggersSingle StateTrigger bound to a composed VM boolean
EventTriggerFrameworkElement.TriggersEventTriggerBehavior (Community Toolkit) or code-behind Storyboard.Begin()
Property Trigger

Property Trigger Becomes VisualState

WPF (Before)
<Style TargetType="Button">
  <Setter Property="Opacity" Value="0.5" />
  <Style.Triggers>
    <Trigger Property="IsMouseOver" Value="True">
      <Setter Property="Opacity" Value="1.0" />
    </Trigger>
  </Style.Triggers>
</Style>
WinUI / Uno (After)
<ControlTemplate TargetType="Button">
  <Grid x:Name="RootGrid" Opacity="0.5">
    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup x:Name="CommonStates">
        <VisualState x:Name="Normal" />
        <VisualState x:Name="PointerOver">
          <VisualState.Setters>
            <Setter Target="RootGrid.Opacity" Value="1.0" />
          </VisualState.Setters>
        </VisualState>
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    <ContentPresenter />
  </Grid>
</ControlTemplate>

Two things to notice. The state is named PointerOver, not IsMouseOver, because the WinUI common states contract uses pointer vocabulary. The VisualStateGroup is attached to RootGrid (the first child of the template), which is a documented requirement for state triggers to work.

DataTrigger

DataTrigger Becomes a StateTrigger or a Behavior

WPF (Before)
<Style TargetType="TextBlock">
  <Style.Triggers>
    <DataTrigger Binding="{Binding IsBusy}" Value="True">
      <Setter Property="Foreground" Value="Gray" />
    </DataTrigger>
  </Style.Triggers>
</Style>

Option 1: StateTrigger

WinUI / Uno
<Grid x:Name="RootGrid">
  <VisualStateManager.VisualStateGroups>
    <VisualStateGroup x:Name="BusyStates">
      <VisualState x:Name="Busy">
        <VisualState.StateTriggers>
          <StateTrigger IsActive="{x:Bind ViewModel.IsBusy, Mode=OneWay}" />
        </VisualState.StateTriggers>
        <VisualState.Setters>
          <Setter Target="StatusText.Foreground" Value="Gray" />
        </VisualState.Setters>
      </VisualState>
    </VisualStateGroup>
  </VisualStateManager.VisualStateGroups>
  <TextBlock x:Name="StatusText" Text="Loading" />
</Grid>

Option 2: XAML Behaviors

Microsoft's documented migration for DataTrigger is XAML Behaviors via the Microsoft.Xaml.Behaviors.* packages. These give you DataTriggerBehavior, EventTriggerBehavior, and composable actions.

When to Use Which

StateTrigger is the right call when the data condition drives a visual layout change that belongs next to the other states of a page or template. DataTriggerBehavior is a closer one-to-one port of the WPF mental model when you want an action (command invocation, property change on another element) rather than a state transition.

MultiTrigger

MultiTrigger Becomes a Combined StateTrigger

OR vs. AND: A VisualState can hold more than one StateTrigger. But if any (not all) of the triggers are active, the VisualState will be applied. That is OR semantics, not WPF MultiTrigger's AND semantics. A direct port of a MultiTrigger with two conditions into two StateTrigger instances changes the meaning.

The safe port is to combine the conditions in a view model boolean and bind a single StateTrigger:

WinUI / Uno
<VisualState x:Name="HoverAndEnabled">
  <VisualState.StateTriggers>
    <StateTrigger IsActive="{x:Bind ViewModel.IsHoverAndEnabled, Mode=OneWay}" />
  </VisualState.StateTriggers>
  <VisualState.Setters>
    <Setter Target="RootGrid.Background" Value="LightBlue" />
  </VisualState.Setters>
</VisualState>

Expose IsHoverAndEnabled as a computed boolean in the view model. This is more portable across WinUI, Uno Platform targets, and MVUX-style reactive feeds than trying to replicate MultiTrigger condition arithmetic in XAML.

EventTrigger

EventTrigger Becomes a Behavior or Code-Behind

WPF (Before)
<Button Content="Go">
  <Button.Triggers>
    <EventTrigger RoutedEvent="Button.Click">
      <BeginStoryboard>
        <Storyboard>
          <DoubleAnimation Storyboard.TargetProperty="Opacity"
                           From="1.0" To="0.0" Duration="0:0:0.4" />
        </Storyboard>
      </BeginStoryboard>
    </EventTrigger>
  </Button.Triggers>
</Button>

Option 1: Code-Behind

C#
private void GoButton_Click(object sender, RoutedEventArgs e)
{
    var storyboard = (Storyboard)Resources["FadeOut"];
    storyboard.Begin();
}

Option 2: EventTriggerBehavior

WinUI / Uno
<Button Content="Click to navigate"
        xmlns:behaviors="using:CommunityToolkit.WinUI.Behaviors"
        xmlns:interactivity="using:Microsoft.Xaml.Interactivity">
  <interactivity:Interaction.Behaviors>
    <interactivity:EventTriggerBehavior EventName="Click">
      <behaviors:NavigateToUriAction
          NavigateUri="https://aka.ms/toolkit/windows" />
    </interactivity:EventTriggerBehavior>
  </interactivity:Interaction.Behaviors>
</Button>

Substitute CallMethodAction, ChangePropertyAction, or any other action the Community Toolkit Behaviors page documents for the inner action element.

Gotchas

Gotchas: Inheritance, Ordering, and Storyboards

Three WPF habits that do not survive the trip:

  • Style BasedOn inheritance works in WinUI 3. However, WPF Style.Triggers do not have a direct equivalent that inherits down style chains. Visual states live in templates, not styles.
  • Trigger ordering changes. WPF gives the last matching trigger precedence within a set of property setters. VisualStateManager has a simpler mental model: only one VisualState per VisualStateGroup is current at a time, and transitioning to a new state reverts the previous state's setters automatically.
  • WPF allows Storyboard inside BeginStoryboard under an EventTrigger. WinUI supports Storyboard inside VisualState as the content property, but for event-driven animations outside of state changes, start the storyboard from code-behind with Storyboard.Begin() or use a Behavior.

VisualStateGroup placement: The VisualStateGroup must be declared under the first child of the root of a control template. If your root is a Grid, attach VisualStateManager.VisualStateGroups to that Grid, not to the Page or a sibling StackPanel.

FAQ

FAQ

Can I still use Trigger in Uno Platform or WinUI 3?

No. System.Windows.Trigger lives in PresentationFramework.dll and is not part of WinUI 3 or Uno Platform. WinUI 3 does expose a Microsoft.UI.Xaml.EventTrigger class, but it is the Silverlight-compatibility type, not the WPF property trigger.

Do I need MVUX, or is x:Bind enough?

For most StateTrigger.IsActive scenarios, a OneWay x:Bind to a view model boolean is enough. MVUX becomes valuable when you want the underlying state to come from a reactive feed rather than a plain property.

How do I handle a DataTrigger that fires on a collection property?

For boolean derivations such as HasItems or Count > 0, compute the boolean in the view model and bind StateTrigger.IsActive to it. For more elaborate behaviors such as "run an animation when an item is added," use a Behavior or a code-behind handler attached to the collection's change notifications.

What about Interaction.Triggers from the Blend SDK?

The successor path is the Microsoft.Xaml.Behaviors.* family, which is what Microsoft recommends for WPF DataTrigger and MultiTrigger migration. In practice, use CommunityToolkit.WinUI.Behaviors with the xmlns:interactivity="using:Microsoft.Xaml.Interactivity" namespace.

Next Step

Wire the first VisualStateGroup on a real template. If you are driving the conversion with an agent, Uno Platform publishes guidance for Claude Code with Uno Platform and for the Uno Platform MCP servers that feed the agent source-verified snippets for VisualStateManager, StateTrigger, and Behaviors.