Erstellen eines Steuerelements mit einer anpassbaren Darstellung

Windows Presentation Foundation (WPF) bietet Ihnen die Möglichkeit, ein Steuerelement zu erstellen, dessen Darstellung angepasst werden kann. Sie können z. B. die Darstellung einer CheckBox-Instanz über die Einstellungseigenschaften hinaus ändern, indem Sie eine neue ControlTemplate-Instanz erstellen. Die folgende Abbildung zeigt eine CheckBox-Instanz, die eine standardmäßige ControlTemplate-Instanz verwendet und eine CheckBox-Instanz, die eine benutzerdefinierte ControlTemplate-Instanz verwendet.

A checkbox with the default control template. Kontrollkästchen, für das die Standardsteuerelementvorlage verwendet wird

A checkbox with a custom control template. Kontrollkästchen, für das eine benutzerdefinierte Steuerelementvorlage verwendet wird

Wenn Sie beim Erstellen eines Steuerelements dem Teile- und Zustandsmodell folgen, können Sie die Darstellung Ihres Steuerelements anpassen. Designer-Tools wie Blend für Visual Studio unterstützen das Teile- und Zustandsmodell, sodass Ihr Steuerelement, wenn Sie diesem Modell folgen, in diesen Arten von Anwendungen anpassbar ist. In diesem Thema werden das Teile- und Zustandsmodell sowie die Befolgung dieses Modells beim Erstellen Ihres eigenen Steuerelements behandelt. In diesem Thema wird ein Beispiel für ein benutzerdefiniertes Steuerelement verwendet, NumericUpDown, um die Philosophie dieses Modells zu veranschaulichen. Das NumericUpDown-Steuerelement zeigt einen numerischen Wert an, den ein Benutzer erhöhen oder verringern kann, indem er auf die Schaltflächen des Steuerelements klickt. Die folgende Abbildung zeigt das NumericUpDown-Steuerelement, das in diesem Thema behandelt wird.

NumericUpDown custom control. Benutzerdefiniertes NumericUpDown-Steuerelement

Dieses Thema enthält folgende Abschnitte:

Voraussetzungen

Dieses Thema setzt voraus, dass Sie wissen, wie Sie eine neue ControlTemplate-Instanz für ein bestehendes Steuerelement erstellen können, dass Sie mit den Elementen eines Steuerelementvertrags vertraut sind und dass Sie die unter Erstellen einer Vorlage für ein Steuerelement besprochenen Konzepte verstehen.

Hinweis

Zum Erstellen eines Steuerelements, dessen Darstellung angepasst werden kann, müssen Sie ein Steuerelement erstellen, das von der Control-Klasse oder einer ihrer Unterklassen (mit Ausnahme von UserControl) erbt. Ein Steuerelement, das von UserControl erbt, ist ein Steuerelement, das schnell erstellt werden kann, aber es verwendet keine ControlTemplate-Instanz, und Sie können seine Darstellung nicht anpassen.

Teile- und Zustandsmodell

Das Teile- und Zustandsmodell legt fest, wie die visuelle Struktur und das visuelle Verhalten eines Steuerelements zu definieren sind. Gehen Sie wie folgt vor, um das Teile- und Zustandsmodell zu befolgen:

  • Definieren Sie die visuelle Struktur und das visuelle Verhalten in der ControlTemplate-Instanz eines Steuerelements.

  • Befolgen Sie bestimmte bewährte Methoden, wenn die Logik Ihres Steuerelements mit Teilen der Steuerelementvorlage interagiert.

  • Stellen Sie einen Steuerelementvertrag zur Verfügung, um festzulegen, was in die ControlTemplate-Instanz einbezogen werden soll.

Wenn Sie die visuelle Struktur und das visuelle Verhalten in der ControlTemplate-Instanz eines Steuerelements definieren, können Ersteller von Anwendungen die visuelle Struktur und das visuelle Verhalten Ihres Steuerelements ändern, indem sie eine neue ControlTemplate-Instanz erstellen, anstatt Code zu schreiben. Sie müssen einen Steuerelementvertrag bereitstellen, der den Erstellern der Anwendung mitteilt, welche FrameworkElement-Objekte und Zustände in der ControlTemplate-Instanz definiert werden sollten. Sie sollten einige bewährte Methoden befolgen, wenn Sie mit den Teilen in der ControlTemplate-Instanz interagieren, sodass Ihr Steuerelement eine unvollständige ControlTemplate-Instanz ordnungsgemäß behandelt. Wenn Sie diese drei Prinzipien befolgen, können Anwendungsersteller für Ihr Steuerelement genauso einfach eine ControlTemplate-Instanz erstellen wie für die Steuerelemente, die mit WPF ausgeliefert werden. Im folgenden Abschnitt werden die einzelnen Empfehlungen ausführlich erläutert.

Definieren der visuellen Struktur und des visuellen Verhaltens eines Steuerelements in einer ControlTemplate-Instanz

Wenn Sie Ihr benutzerdefiniertes Steuerelement mithilfe des Teile- und Zustandsmodells erstellen, definieren Sie die visuelle Struktur und das visuelle Verhalten des Steuerelements in seiner ControlTemplate-Instanz und nicht in seiner Logik. Die visuelle Struktur eines Steuerelements ist die Zusammensetzung der FrameworkElement-Objekte, aus denen das Steuerelement besteht. Das visuelle Verhalten ist die Art und Weise, wie das Steuerelement angezeigt wird, wenn es sich in einem bestimmten Zustand befindet. Weitere Informationen zum Erstellen eines ControlTemplate-Steuerelements, das die visuelle Struktur und das visuelle Verhalten eines Steuerelements angibt, finden Sie unter Erstellen einer Vorlage für ein Steuerelement.

Im Beispiel des NumericUpDown-Steuerelements enthält die visuelle Struktur zwei RepeatButton-Steuerelemente und ein TextBlock-Steuerelement. Wenn Sie diese Steuerelemente im Code des NumericUpDown-Steuerelements hinzufügen, z. B. in seinem Konstruktor, sind die Positionen dieser Steuerelemente unveränderlich. Anstatt die visuelle Struktur und das visuelle Verhalten des Steuerelements in seinem Code zu definieren, sollten Sie es in der ControlTemplate-Instanz definieren. Dann kann ein Anwendungsentwickler die Position der Schaltflächen und der TextBlock-Instanz anpassen und das Verhalten festlegen, wenn Value negativ ist, da die ControlTemplate-Instanz ersetzt werden kann.

Das folgende Beispiel zeigt die visuelle Struktur des NumericUpDown-Steuerelements, das eine RepeatButton-Instanz zum Erhöhen von Value, eine RepeatButton-Instanz zum Verringern von Value sowie eine TextBlock-Instanz zum Anzeigen von Value enthält.

<ControlTemplate TargetType="src:NumericUpDown">
  <Grid  Margin="3" 
         Background="{TemplateBinding Background}">
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
      </Grid.RowDefinitions>
      <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
      </Grid.ColumnDefinitions>

      <Border BorderThickness="1" BorderBrush="Gray" 
              Margin="7,2,2,2" Grid.RowSpan="2" 
              Background="#E0FFFFFF"
              VerticalAlignment="Center" 
              HorizontalAlignment="Stretch">

        <!--Bind the TextBlock to the Value property-->
        <TextBlock Name="TextBlock"
                   Width="60" TextAlignment="Right" Padding="5"
                   Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                     AncestorType={x:Type src:NumericUpDown}}, 
                     Path=Value}"/>
      </Border>

      <RepeatButton Content="Up" Margin="2,5,5,0"
        Name="UpButton"
        Grid.Column="1" Grid.Row="0"/>
      <RepeatButton Content="Down" Margin="2,0,5,5"
        Name="DownButton"
        Grid.Column="1" Grid.Row="1"/>

      <Rectangle Name="FocusVisual" Grid.ColumnSpan="2" Grid.RowSpan="2" 
        Stroke="Black" StrokeThickness="1"  
        Visibility="Collapsed"/>
    </Grid>

  </Grid>
</ControlTemplate>

Ein visuelles Verhalten des NumericUpDown-Steuerelements besteht darin, dass der Wert eine rote Schriftart aufweist, wenn es negativ ist. Wenn Sie den Foreground von TextBlock im Code ändern, wenn Value negativ ist, wird NumericUpDown immer einen roten negativen Wert anzeigen. Sie legen das visuelle Verhalten des Steuerelements in der ControlTemplate-Instanz fest, indem Sie VisualState-Objekte zu ControlTemplate hinzufügen. Das folgende Beispiel zeigt die VisualState-Objekte für die Zustände Positive und Negative an. Positive und Negative schließen sich gegenseitig aus (das Steuerelement befindet sich immer in genau einem der beiden), sodass das Beispiel die VisualState-Objekte in eine einzelne VisualStateGroup-Instanz platziert. Wenn das Steuerelement in den Zustand Negative wechselt, wird der Foreground der TextBlock-Instanz entsprechend rot. Wenn sich das Steuerelement im Zustand Positive befindet, kehrt der Foreground zu seinem ursprünglichen Wert zurück. Das Definieren von VisualState-Objekten in einer ControlTemplate-Instanz wird unter Erstellen einer Vorlage für ein Steuerelement näher erläutert.

Hinweis

Stellen Sie sicher, dass Sie die an VisualStateManager.VisualStateGroups angefügte Eigenschaft für die FrameworkElement-Stamminstanz der ControlTemplate-Instanz festlegen.

<ControlTemplate TargetType="local:NumericUpDown">
  <Grid  Margin="3" 
         Background="{TemplateBinding Background}">

    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup Name="ValueStates">

        <!--Make the Value property red when it is negative.-->
        <VisualState Name="Negative">
          <Storyboard>
            <ColorAnimation To="Red"
              Storyboard.TargetName="TextBlock" 
              Storyboard.TargetProperty="(Foreground).(Color)"/>
          </Storyboard>

        </VisualState>

        <!--Return the TextBlock's Foreground to its 
            original color.-->
        <VisualState Name="Positive"/>
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
  </Grid>
</ControlTemplate>

Verwenden von Teilen der ControlTemplate-Instanz im Code

Der Ersteller einer ControlTemplate-Instanz kann absichtlich oder versehentlich FrameworkElement- oder VisualState-Objekte auslassen, aber die Logik Ihres Steuerelements benötigt diese Teile möglicherweise, um richtig zu funktionieren. Das Teile- und Zustandsmodell legt fest, dass Ihr Steuerelement resilient auf ein ControlTemplate reagieren sollte, dem FrameworkElement- oder VisualState-Objekte fehlen. Ihr Steuerelement sollte keine Ausnahme auslösen oder einen Fehler melden, wenn eine FrameworkElement-, VisualState- oder VisualStateGroup-Instanz in der ControlTemplate-Instanz fehlt. In diesem Abschnitt werden die empfohlenen Methoden für die Interaktion mit FrameworkElement-Objekten und verwaltenden Zuständen beschrieben.

Erwarten fehlender FrameworkElement-Objekte

Wenn Sie FrameworkElement-Objekte in der ControlTemplate-Instanz definieren, muss die Logik des Steuerelements möglicherweise mit einigen von ihnen interagieren. Beispielsweise abonniert das NumericUpDown-Steuerelement das Click-Ereignis der Schaltflächen, um Value zu erhöhen oder zu verringern und die Text-Eigenschaft der TextBlock-Instanz auf Value festzulegen. Wenn eine benutzerdefinierte ControlTemplate-Instanz die TextBlock-Instanz oder Schaltflächen auslässt, ist es akzeptabel, dass das Steuerelement einen Teil seiner Funktionalität verliert, aber Sie sollten sicher sein, dass Ihr Steuerelement keinen Fehler verursacht. Wenn z. B. eine ControlTemplate-Instanz nicht die Schaltflächen zum Ändern von Value enthält, verliert NumericUpDown diese Funktion, aber eine Anwendung, die ControlTemplate-Instanz verwendet, wird weiterhin ausgeführt.

Die folgenden Methoden stellen sicher, dass Ihr Steuerelement ordnungsgemäß auf fehlende FrameworkElement-Objekte reagiert:

  1. Legen Sie das x:Name-Attribut für jede FrameworkElement-Instanz fest, auf die Sie im Code verweisen müssen.

  2. Definieren Sie für jede FrameworkElement-Instanz, mit der Sie interagieren müssen, private Eigenschaften.

  3. Sie können alle Ereignisse, die Ihr Steuerelement verarbeitet, über die festgelegte Zugriffsmethode der FrameworkElement-Eigenschaft abonnieren und abbestellen.

  4. Legen Sie die FrameworkElement-Eigenschaften fest, die Sie in Schritt 2 in der OnApplyTemplate-Methode definiert haben. Dies ist der früheste Zeitpunkt, an dem die FrameworkElement-Instanz in der ControlTemplate-Instanz für das Steuerelement verfügbar ist. Verwenden Sie x:Name der FrameworkElement-Instanz, um es von der ControlTemplate abzurufen.

  5. Vergewissern Sie sich, dass FrameworkElement nicht null ist, bevor Sie auf dessen Member zugreifen. Wenn es null ist, melden Sie keinen Fehler.

Die folgenden Beispiele zeigen, wie das NumericUpDown-Steuerelement gemäß den Empfehlungen in der vorangegangenen Liste mit FrameworkElement-Objekten interagiert.

In dem Beispiel, in dem die visuelle Struktur des NumericUpDown-Steuerelements in der ControlTemplate-Instanz definiert wird, ist für die RepeatButton-Instanz, die Value erhöht, das x:Name-Attribut auf UpButton festgelegt. Das folgende Beispiel deklariert eine Eigenschaft mit dem Namen UpButtonElement, die die RepeatButton-Instanz darstellt, die in der ControlTemplate-Instanz deklariert ist. Die set-Zugriffsmethode bestellt zunächst das Click-Ereignis der Schaltfläche ab, wenn UpDownElement nicht null ist. Dann legt sie die Eigenschaft fest und abonniert dann das Click-Ereignis. Es gibt auch eine Eigenschaft, die aber hier nicht gezeigt wird, für die andere RepeatButton-Instanz namens DownButtonElement.

private RepeatButton upButtonElement;

private RepeatButton UpButtonElement
{
    get
    {
        return upButtonElement;
    }

    set
    {
        if (upButtonElement != null)
        {
            upButtonElement.Click -=
                new RoutedEventHandler(upButtonElement_Click);
        }
        upButtonElement = value;

        if (upButtonElement != null)
        {
            upButtonElement.Click +=
                new RoutedEventHandler(upButtonElement_Click);
        }
    }
}
Private m_upButtonElement As RepeatButton

Private Property UpButtonElement() As RepeatButton
    Get
        Return m_upButtonElement
    End Get

    Set(ByVal value As RepeatButton)
        If m_upButtonElement IsNot Nothing Then
            RemoveHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
        End If
        m_upButtonElement = value

        If m_upButtonElement IsNot Nothing Then
            AddHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
        End If
    End Set
End Property

Das folgende Beispiel zeigt die OnApplyTemplate-Instanz für das NumericUpDown-Steuerelement. Das Beispiel verwendet die GetTemplateChild-Methode zum Abrufen der FrameworkElement-Objekte aus der ControlTemplate-Instanz. Beachten Sie, dass das Beispiel vor Fällen schützt, in denen GetTemplateChild eine FrameworkElement-Instanz mit dem angegebenen Namen findet, die nicht den erwarteten Typ aufweist. Es ist auch eine bewährte Methode, Elemente zu ignorieren, die zwar die angegebene x:Name-Instanz aufweisen, aber vom falschen Typ sind.

public override void OnApplyTemplate()
{
    UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
    DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
    //TextElement = GetTemplateChild("TextBlock") as TextBlock;

    UpdateStates(false);
}
Public Overloads Overrides Sub OnApplyTemplate()

    UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
    DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)

    UpdateStates(False)
End Sub

Indem Sie die in den vorherigen Beispielen gezeigten Praktiken befolgen, stellen Sie sicher, dass Ihr Steuerelement weiterhin ausgeführt wird, wenn der ControlTemplate-Instanz ein FrameworkElement fehlt.

Verwenden von VisualStateManager zum Verwalten von Zuständen

Der VisualStateManager verfolgt die Zustände eines Steuerelements nach und führt die für den Übergang zwischen den Zuständen erforderliche Logik aus. Wenn Sie VisualStateObjekte zur ControlTemplate-Instanz hinzufügen, fügen Sie sie zu einer VisualStateGroup-Instanz hinzu und fügen die VisualStateGroup-Instanz zu der an VisualStateManager.VisualStateGroups angefügten Eigenschaft hinzu, sodass der VisualStateManager Zugriff auf sie hat.

Das folgende Beispiel wiederholt das vorherige Beispiel, das die VisualState-Objekte zeigt, die den Positive- und Negative-Zuständen des Steuerelements entsprechen. Die Storyboard-Instanz in NegativeVisualState zeigt die Foreground-Instanz der TextBlock-Instanz rot an. Wenn das NumericUpDown-Steuerelement den Zustand Negative aufweist, beginnt das Storyboard mit dem Zustand Negative. Dann wird das Storyboard im Zustand Negative beendet, wenn das Steuerelement in den Zustand Positive zurückkehrt. Die PositiveVisualState-Instanz muss kein Storyboard enthalten, denn wenn das Storyboard für Negative beendet wird, kehrt der Foreground zu seiner ursprünglichen Farbe zurück.

<ControlTemplate TargetType="local:NumericUpDown">
  <Grid  Margin="3" 
         Background="{TemplateBinding Background}">

    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup Name="ValueStates">

        <!--Make the Value property red when it is negative.-->
        <VisualState Name="Negative">
          <Storyboard>
            <ColorAnimation To="Red"
              Storyboard.TargetName="TextBlock" 
              Storyboard.TargetProperty="(Foreground).(Color)"/>
          </Storyboard>

        </VisualState>

        <!--Return the TextBlock's Foreground to its 
            original color.-->
        <VisualState Name="Positive"/>
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
  </Grid>
</ControlTemplate>

Beachten Sie, dass der TextBlock einen Namen erhält, aber der TextBlock ist nicht im Steuerelementvertrag für NumericUpDown enthalten, da die Logik des Steuerelements niemals auf den TextBlock verweist. Elemente, auf die in der ControlTemplate-Instanz verwiesen wird, verfügen über Namen, müssen aber nicht Teil des Steuerelementvertrags sein, da eine neue ControlTemplate-Instanz für das Steuerelement möglicherweise nicht auf dieses Element verweisen muss. Beispielsweise könnte jemand, der eine neue ControlTemplate-Instanz für NumericUpDown erstellt, sich dazu entscheiden, nicht anzugeben, dass Value negativ ist, indem der Foreground geändert wird. In diesem Fall verweisen weder Code noch die ControlTemplate-Instanz auf die TextBlock-Instanz über den Namen.

Die Logik des Steuerelements ist dafür verantwortlich, den Zustand des Steuerelements zu ändern. Das folgende Beispiel zeigt, dass das NumericUpDown-Steuerelement die GoToState-Methode aufruft, um in den Zustand Positive zu wechseln, wenn Value entsprechend 0 oder größer ist, und in Zustand Negative, wenn Value kleiner als 0 ist.

if (Value >= 0)
{
    VisualStateManager.GoToState(this, "Positive", useTransitions);
}
else
{
    VisualStateManager.GoToState(this, "Negative", useTransitions);
}
If Value >= 0 Then
    VisualStateManager.GoToState(Me, "Positive", useTransitions)
Else
    VisualStateManager.GoToState(Me, "Negative", useTransitions)
End If

Die GoToState-Methode führt die notwendige Logik aus, um die Storyboards entsprechend zu starten und zu beenden. Wenn ein Steuerelement GoToState aufruft, um seinen Zustand zu ändern, geht der VisualStateManager wie folgt vor:

  • Wenn der VisualState, in den das Steuerelement wechselt, über ein Storyboard verfügt, dann beginnt das Storyboard. Wenn der VisualState, aus dem das Steuerelement wechselt, über ein Storyboard verfügt, wird das Storyboard beendet.

  • Wenn sich das Steuerelement bereits in dem angegebenen Zustand befindet, führt GoToState keine Aktion aus und gibt true zurück.

  • Wenn der angegebene Zustand nicht in der ControlTemplate-Instanz von control vorhanden ist, führt GoToState keine Aktion aus und gibt false zurück.

Bewährte Methoden für die Arbeit mit dem VisualStateManager

Es wird empfohlen, dass Sie wie folgt vorgehen, um die Zustände Ihres Steuerelements beizubehalten:

  • Verwenden Sie Eigenschaften zum Nachverfolgen des Zustands.

  • Erstellen Sie eine Hilfsmethode für den Übergang zwischen Zuständen.

Das NumericUpDown-Steuerelement verwendet seine Value-Eigenschaft, um nachzuverfolgen, ob es sich im Zustand Positive oder Negative befindet. Das NumericUpDown-Steuerelement definiert auch die Zustände Focused und UnFocused, wodurch die IsFocused-Eigenschaft nachverfolgt wird. Wenn Sie Zustände verwenden, die nicht natürlich einer Eigenschaft des Steuerelements entsprechen, können Sie eine private Eigenschaft definieren, um den Zustand nachzuverfolgen.

Eine einzelne Methode, die alle Zustände aktualisiert, zentralisiert die Aufrufe an den VisualStateManager und hält Ihren Code überschaubar. Das folgende Beispiel zeigt die Hilfsmethode des NumericUpDown-Steuerelements, UpdateStates. Wenn Value größer als oder gleich 0 ist, befindet sich die Control-Instanz im Zustand Positive. Ist Value kleiner als 0, befindet sich das Steuerelement im Zustand Negative. Wenn IsFocused entsprechend true ist, befindet sich das Steuerelement im Zustand Focused. Andernfalls befindet es sich im Zustand Unfocused. Das Steuerelement kann UpdateStates immer dann aufrufen, wenn es seinen Zustand ändern muss, unabhängig davon, welcher Zustand sich ändert.

private void UpdateStates(bool useTransitions)
{
    if (Value >= 0)
    {
        VisualStateManager.GoToState(this, "Positive", useTransitions);
    }
    else
    {
        VisualStateManager.GoToState(this, "Negative", useTransitions);
    }

    if (IsFocused)
    {
        VisualStateManager.GoToState(this, "Focused", useTransitions);
    }
    else
    {
        VisualStateManager.GoToState(this, "Unfocused", useTransitions);
    }
}
Private Sub UpdateStates(ByVal useTransitions As Boolean)

    If Value >= 0 Then
        VisualStateManager.GoToState(Me, "Positive", useTransitions)
    Else
        VisualStateManager.GoToState(Me, "Negative", useTransitions)
    End If

    If IsFocused Then
        VisualStateManager.GoToState(Me, "Focused", useTransitions)
    Else
        VisualStateManager.GoToState(Me, "Unfocused", useTransitions)

    End If
End Sub

Wenn Sie einen Zustandsnamen an GoToState übergeben, wenn sich das Steuerelement bereits in diesem Zustand befindet, unternimmt GoToState nichts, sodass Sie nicht den aktuellen Zustand des Steuerelements ermitteln müssen. Wenn Value z. B. von einer negativen Zahl zu einer anderen negativen Zahl wechselt, wird das Storyboard für den Zustand Negative nicht unterbrochen und der Benutzer sieht keine Änderung am Steuerelement.

Der VisualStateManager verwendet VisualStateGroup-Objekte, um zu bestimmen, welcher Zustand verlassen werden soll, wenn Sie GoToState aufrufen. Das Steuerelement befindet sich immer in einem Zustand für jede VisualStateGroup-Instanz, die in seiner ControlTemplate-Instanz definiert ist und verlässt einen Zustand nur, wenn es in einen anderen Zustand aus derselben VisualStateGroup-Instanz wechselt. Beispielsweise definiert die ControlTemplate-Instanz des NumericUpDown-Steuerelements die Positive- und NegativeVisualState-Objekte in einer VisualStateGroup-Instanz und die Focused- und UnfocusedVisualState-Objekte in einer anderen Instanz. (Sie können die Focused- und UnfocusedVisualState-Instanzen im Abschnitt Vollständiges Beispiel in diesem Thema sehen. Wenn das Steuerelement vom Zustand Positive in den Zustand Negative wechselt oder umgekehrt, bleibt das Steuerelement entweder im Zustand Focused oder im Zustand Unfocused.

Es gibt drei typische Stellen, an denen sich der Zustand eines Steuerelements ändern kann:

  • Wenn die ControlTemplate-Instanz auf die Control-Instanz angewendet wird.

  • Wenn eine Eigenschaft geändert wird.

  • Wenn ein Ereignis eintritt.

Die folgenden Beispiele zeigen, wie Sie den Zustand des NumericUpDown-Steuerelements in diesen Fällen aktualisieren.

Sie sollten den Zustand des Steuerelements in der OnApplyTemplate-Methode aktualisieren, sodass das Steuerelement im richtigen Zustand angezeigt wird, wenn die ControlTemplate-Instanz angewendet wird. Das folgende Beispiel ruft UpdateStates in OnApplyTemplate auf, um sicherzustellen, dass sich das Steuerelement in den entsprechenden Zuständen befindet. Nehmen Sie z. B. an, dass Sie ein NumericUpDown-Steuerelement erstellen und dann seinen Foreground auf „Grün“ und den Value auf „-5“ festlegen. Wenn Sie UpdateStates nicht aufrufen, wenn die ControlTemplate-Instanz auf das NumericUpDown-Steuerelement angewendet wird, befindet sich das Steuerelement nicht im Zustand Negative und der Wert ist grün statt rot. Sie müssen UpdateStates aufrufen, um das Steuerelement in den Zustand Negative zu versetzen.

public override void OnApplyTemplate()
{
    UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
    DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
    //TextElement = GetTemplateChild("TextBlock") as TextBlock;

    UpdateStates(false);
}
Public Overloads Overrides Sub OnApplyTemplate()

    UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
    DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)

    UpdateStates(False)
End Sub

Häufig müssen Sie die Zustände eines Steuerelements aktualisieren, wenn sich eine Eigenschaft ändert. Das folgende Beispiel zeigt die gesamte ValueChangedCallback-Methode. Da ValueChangedCallback aufgerufen wird, wenn sich der Value ändert, ruft die Methode UpdateStates auf, wenn sich der Value von positiv zu negativ oder umgekehrt wechselt. Es ist akzeptabel, UpdateStates aufzurufen, wenn sich der Value ändert, aber positiv oder negativ bleibt, denn in diesem Fall ändert das Steuerelement seinen Zustand nicht.

private static void ValueChangedCallback(DependencyObject obj,
    DependencyPropertyChangedEventArgs args)
{
    NumericUpDown ctl = (NumericUpDown)obj;
    int newValue = (int)args.NewValue;

    // Call UpdateStates because the Value might have caused the
    // control to change ValueStates.
    ctl.UpdateStates(true);

    // Call OnValueChanged to raise the ValueChanged event.
    ctl.OnValueChanged(
        new ValueChangedEventArgs(NumericUpDown.ValueChangedEvent,
            newValue));
}
Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject,
                                        ByVal args As DependencyPropertyChangedEventArgs)

    Dim ctl As NumericUpDown = DirectCast(obj, NumericUpDown)
    Dim newValue As Integer = CInt(args.NewValue)

    ' Call UpdateStates because the Value might have caused the
    ' control to change ValueStates.
    ctl.UpdateStates(True)

    ' Call OnValueChanged to raise the ValueChanged event.
    ctl.OnValueChanged(New ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, newValue))
End Sub

Möglicherweise müssen Sie auch Zustände aktualisieren, wenn ein Ereignis eintritt. Das folgende Beispiel zeigt, dass NumericUpDown entsprechend UpdateStates für das Control aufruft, um das GotFocus-Ereignis zu behandeln.

protected override void OnGotFocus(RoutedEventArgs e)
{
    base.OnGotFocus(e);
    UpdateStates(true);
}
Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
    MyBase.OnGotFocus(e)
    UpdateStates(True)
End Sub

Der VisualStateManager hilft Ihnen beim Verwalten der Zustände Ihres Steuerelements. Mit dem VisualStateManager stellen Sie sicher, dass Ihr Steuerelement ordnungsgemäß zwischen Zuständen wechselt. Wenn Sie die in diesem Abschnitt beschriebenen Empfehlungen für die Arbeit mit dem VisualStateManager befolgen, bleibt der Code Ihres Steuerelements lesbar und wartbar.

Bereitstellen des Steuerelementvertrags

Sie stellen einen Steuerelementvertrag bereit, sodass die ControlTemplate-Ersteller wissen, was sie in die Vorlage einfügen müssen. Ein Steuerelementvertrag besteht aus drei Elementen:

  • Den visuellen Elementen, die die Logik des Steuerelements verwenden.

  • Den Zuständen des Steuerelements und den Gruppen, zu denen die einzelnen Zustände gehören.

  • Den öffentlichen Eigenschaften, die die visuelle Darstellung des Steuerelements beeinflussen.

Jemand, der eine neue ControlTemplate-Instanz erstellt, muss wissen, welche FrameworkElement-Objekte die Logik des Steuerelements verwendet, welchen Typ jedes Objekt aufweist und welchen Namen es verwendet. Ein ControlTemplate-Ersteller muss auch den Namen jedes möglichen Zustands kennen, in dem sich das Steuerelement befinden kann, und in welchem Zustand sich die VisualStateGroup befindet.

Um auf das NumericUpDown-Beispiel zurückzukommen, erwartet das Steuerelement, dass die ControlTemplate-Instanz die folgenden FrameworkElement-Objekte umfasst:

Die Steuerung kann sich in den folgenden Zuständen befinden:

Um anzugeben, welche FrameworkElement-Objekte das Steuerelement erwartet, verwenden Sie das TemplatePartAttribute verwenden, das den Namen und den Typ der erwarteten Elemente angibt. Um die möglichen Zustände eines Steuerelements anzugeben, verwenden Sie das TemplateVisualStateAttribute, das den Namen des Zustands und die VisualStateGroup angibt, zu der er gehört. Platzieren Sie das TemplatePartAttribute und das TemplateVisualStateAttribute in die Klassendefinition des Steuerelements.

Jede öffentliche Eigenschaft, die sich auf die Darstellung Ihres Steuerelements auswirkt, ist ebenfalls Teil des Steuerelementvertrags.

Im folgenden Beispiel werden das FrameworkElement-Objekt und die Zustände für das NumericUpDown-Steuerelement angegeben.

[TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
[TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
[TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
[TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
public class NumericUpDown : Control
{
    public static readonly DependencyProperty BackgroundProperty;
    public static readonly DependencyProperty BorderBrushProperty;
    public static readonly DependencyProperty BorderThicknessProperty;
    public static readonly DependencyProperty FontFamilyProperty;
    public static readonly DependencyProperty FontSizeProperty;
    public static readonly DependencyProperty FontStretchProperty;
    public static readonly DependencyProperty FontStyleProperty;
    public static readonly DependencyProperty FontWeightProperty;
    public static readonly DependencyProperty ForegroundProperty;
    public static readonly DependencyProperty HorizontalContentAlignmentProperty;
    public static readonly DependencyProperty PaddingProperty;
    public static readonly DependencyProperty TextAlignmentProperty;
    public static readonly DependencyProperty TextDecorationsProperty;
    public static readonly DependencyProperty TextWrappingProperty;
    public static readonly DependencyProperty VerticalContentAlignmentProperty;

    public Brush Background { get; set; }
    public Brush BorderBrush { get; set; }
    public Thickness BorderThickness { get; set; }
    public FontFamily FontFamily { get; set; }
    public double FontSize { get; set; }
    public FontStretch FontStretch { get; set; }
    public FontStyle FontStyle { get; set; }
    public FontWeight FontWeight { get; set; }
    public Brush Foreground { get; set; }
    public HorizontalAlignment HorizontalContentAlignment { get; set; }
    public Thickness Padding { get; set; }
    public TextAlignment TextAlignment { get; set; }
    public TextDecorationCollection TextDecorations { get; set; }
    public TextWrapping TextWrapping { get; set; }
    public VerticalAlignment VerticalContentAlignment { get; set; }
}
<TemplatePart(Name:="UpButtonElement", Type:=GetType(RepeatButton))>
<TemplatePart(Name:="DownButtonElement", Type:=GetType(RepeatButton))>
<TemplateVisualState(Name:="Positive", GroupName:="ValueStates")>
<TemplateVisualState(Name:="Negative", GroupName:="ValueStates")>
<TemplateVisualState(Name:="Focused", GroupName:="FocusedStates")>
<TemplateVisualState(Name:="Unfocused", GroupName:="FocusedStates")>
Public Class NumericUpDown
    Inherits Control
    Public Shared ReadOnly TextAlignmentProperty As DependencyProperty
    Public Shared ReadOnly TextDecorationsProperty As DependencyProperty
    Public Shared ReadOnly TextWrappingProperty As DependencyProperty

    Public Property TextAlignment() As TextAlignment

    Public Property TextDecorations() As TextDecorationCollection

    Public Property TextWrapping() As TextWrapping
End Class

Vollständiges Beispiel

Das folgende Beispiel ist die gesamte ControlTemplate-Instanz für das NumericUpDown-Steuerelement.

<!--This is the contents of the themes/generic.xaml file.-->
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:VSMCustomControl">


  <Style TargetType="{x:Type local:NumericUpDown}">
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="local:NumericUpDown">
          <Grid  Margin="3" 
                Background="{TemplateBinding Background}">


            <VisualStateManager.VisualStateGroups>

              <VisualStateGroup Name="ValueStates">

                <!--Make the Value property red when it is negative.-->
                <VisualState Name="Negative">
                  <Storyboard>
                    <ColorAnimation To="Red"
                      Storyboard.TargetName="TextBlock" 
                      Storyboard.TargetProperty="(Foreground).(Color)"/>
                  </Storyboard>

                </VisualState>

                <!--Return the control to its initial state by
                    return the TextBlock's Foreground to its 
                    original color.-->
                <VisualState Name="Positive"/>
              </VisualStateGroup>

              <VisualStateGroup Name="FocusStates">

                <!--Add a focus rectangle to highlight the entire control
                    when it has focus.-->
                <VisualState Name="Focused">
                  <Storyboard>
                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusVisual" 
                                                   Storyboard.TargetProperty="Visibility" Duration="0">
                      <DiscreteObjectKeyFrame KeyTime="0">
                        <DiscreteObjectKeyFrame.Value>
                          <Visibility>Visible</Visibility>
                        </DiscreteObjectKeyFrame.Value>
                      </DiscreteObjectKeyFrame>
                    </ObjectAnimationUsingKeyFrames>
                  </Storyboard>
                </VisualState>

                <!--Return the control to its initial state by
                    hiding the focus rectangle.-->
                <VisualState Name="Unfocused"/>
              </VisualStateGroup>

            </VisualStateManager.VisualStateGroups>

            <Grid>
              <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
              </Grid.RowDefinitions>
              <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
              </Grid.ColumnDefinitions>

              <Border BorderThickness="1" BorderBrush="Gray" 
                Margin="7,2,2,2" Grid.RowSpan="2" 
                Background="#E0FFFFFF"
                VerticalAlignment="Center" 
                HorizontalAlignment="Stretch">
                <!--Bind the TextBlock to the Value property-->
                <TextBlock Name="TextBlock"
                  Width="60" TextAlignment="Right" Padding="5"
                  Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                                 AncestorType={x:Type local:NumericUpDown}}, 
                                 Path=Value}"/>
              </Border>

              <RepeatButton Content="Up" Margin="2,5,5,0"
                Name="UpButton"
                Grid.Column="1" Grid.Row="0"/>
              <RepeatButton Content="Down" Margin="2,0,5,5"
                Name="DownButton"
                Grid.Column="1" Grid.Row="1"/>

              <Rectangle Name="FocusVisual" Grid.ColumnSpan="2" Grid.RowSpan="2" 
                Stroke="Black" StrokeThickness="1"  
                Visibility="Collapsed"/>
            </Grid>

          </Grid>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>

Das folgende Beispiel zeigt die Logik für die NumericUpDown-Instanz.

using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;

namespace VSMCustomControl
{
    [TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
    [TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
    [TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
    [TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
    [TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
    [TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
    public class NumericUpDown : Control
    {
        public NumericUpDown()
        {
            DefaultStyleKey = typeof(NumericUpDown);
            this.IsTabStop = true;
        }

        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register(
                "Value", typeof(int), typeof(NumericUpDown),
                new PropertyMetadata(
                    new PropertyChangedCallback(ValueChangedCallback)));

        public int Value
        {
            get
            {
                return (int)GetValue(ValueProperty);
            }

            set
            {
                SetValue(ValueProperty, value);
            }
        }

        private static void ValueChangedCallback(DependencyObject obj,
            DependencyPropertyChangedEventArgs args)
        {
            NumericUpDown ctl = (NumericUpDown)obj;
            int newValue = (int)args.NewValue;

            // Call UpdateStates because the Value might have caused the
            // control to change ValueStates.
            ctl.UpdateStates(true);

            // Call OnValueChanged to raise the ValueChanged event.
            ctl.OnValueChanged(
                new ValueChangedEventArgs(NumericUpDown.ValueChangedEvent,
                    newValue));
        }

        public static readonly RoutedEvent ValueChangedEvent =
            EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Direct,
                          typeof(ValueChangedEventHandler), typeof(NumericUpDown));

        public event ValueChangedEventHandler ValueChanged
        {
            add { AddHandler(ValueChangedEvent, value); }
            remove { RemoveHandler(ValueChangedEvent, value); }
        }

        protected virtual void OnValueChanged(ValueChangedEventArgs e)
        {
            // Raise the ValueChanged event so applications can be alerted
            // when Value changes.
            RaiseEvent(e);
        }

        private void UpdateStates(bool useTransitions)
        {
            if (Value >= 0)
            {
                VisualStateManager.GoToState(this, "Positive", useTransitions);
            }
            else
            {
                VisualStateManager.GoToState(this, "Negative", useTransitions);
            }

            if (IsFocused)
            {
                VisualStateManager.GoToState(this, "Focused", useTransitions);
            }
            else
            {
                VisualStateManager.GoToState(this, "Unfocused", useTransitions);
            }
        }

        public override void OnApplyTemplate()
        {
            UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
            DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
            //TextElement = GetTemplateChild("TextBlock") as TextBlock;

            UpdateStates(false);
        }

        private RepeatButton downButtonElement;

        private RepeatButton DownButtonElement
        {
            get
            {
                return downButtonElement;
            }

            set
            {
                if (downButtonElement != null)
                {
                    downButtonElement.Click -=
                        new RoutedEventHandler(downButtonElement_Click);
                }
                downButtonElement = value;

                if (downButtonElement != null)
                {
                    downButtonElement.Click +=
                        new RoutedEventHandler(downButtonElement_Click);
                }
            }
        }

        void downButtonElement_Click(object sender, RoutedEventArgs e)
        {
            Value--;
        }

        private RepeatButton upButtonElement;

        private RepeatButton UpButtonElement
        {
            get
            {
                return upButtonElement;
            }

            set
            {
                if (upButtonElement != null)
                {
                    upButtonElement.Click -=
                        new RoutedEventHandler(upButtonElement_Click);
                }
                upButtonElement = value;

                if (upButtonElement != null)
                {
                    upButtonElement.Click +=
                        new RoutedEventHandler(upButtonElement_Click);
                }
            }
        }

        void upButtonElement_Click(object sender, RoutedEventArgs e)
        {
            Value++;
        }

        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            base.OnMouseLeftButtonDown(e);
            Focus();
        }


        protected override void OnGotFocus(RoutedEventArgs e)
        {
            base.OnGotFocus(e);
            UpdateStates(true);
        }

        protected override void OnLostFocus(RoutedEventArgs e)
        {
            base.OnLostFocus(e);
            UpdateStates(true);
        }
    }

    public delegate void ValueChangedEventHandler(object sender, ValueChangedEventArgs e);

    public class ValueChangedEventArgs : RoutedEventArgs
    {
        private int _value;

        public ValueChangedEventArgs(RoutedEvent id, int num)
        {
            _value = num;
            RoutedEvent = id;
        }

        public int Value
        {
            get { return _value; }
        }
    }
}
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Controls.Primitives
Imports System.Windows.Input
Imports System.Windows.Media

<TemplatePart(Name:="UpButtonElement", Type:=GetType(RepeatButton))> _
<TemplatePart(Name:="DownButtonElement", Type:=GetType(RepeatButton))> _
<TemplateVisualState(Name:="Positive", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Negative", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Focused", GroupName:="FocusedStates")> _
<TemplateVisualState(Name:="Unfocused", GroupName:="FocusedStates")> _
Public Class NumericUpDown
    Inherits Control

    Public Sub New()
        DefaultStyleKeyProperty.OverrideMetadata(GetType(NumericUpDown), New FrameworkPropertyMetadata(GetType(NumericUpDown)))
        Me.IsTabStop = True
    End Sub

    Public Shared ReadOnly ValueProperty As DependencyProperty =
        DependencyProperty.Register("Value", GetType(Integer), GetType(NumericUpDown),
                          New PropertyMetadata(New PropertyChangedCallback(AddressOf ValueChangedCallback)))

    Public Property Value() As Integer

        Get
            Return CInt(GetValue(ValueProperty))
        End Get

        Set(ByVal value As Integer)

            SetValue(ValueProperty, value)
        End Set
    End Property

    Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject,
                                            ByVal args As DependencyPropertyChangedEventArgs)

        Dim ctl As NumericUpDown = DirectCast(obj, NumericUpDown)
        Dim newValue As Integer = CInt(args.NewValue)

        ' Call UpdateStates because the Value might have caused the
        ' control to change ValueStates.
        ctl.UpdateStates(True)

        ' Call OnValueChanged to raise the ValueChanged event.
        ctl.OnValueChanged(New ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, newValue))
    End Sub

    Public Shared ReadOnly ValueChangedEvent As RoutedEvent =
        EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Direct,
                                         GetType(ValueChangedEventHandler), GetType(NumericUpDown))

    Public Custom Event ValueChanged As ValueChangedEventHandler

        AddHandler(ByVal value As ValueChangedEventHandler)
            Me.AddHandler(ValueChangedEvent, value)
        End AddHandler

        RemoveHandler(ByVal value As ValueChangedEventHandler)
            Me.RemoveHandler(ValueChangedEvent, value)
        End RemoveHandler

        RaiseEvent(ByVal sender As Object, ByVal e As RoutedEventArgs)
            Me.RaiseEvent(e)
        End RaiseEvent

    End Event


    Protected Overridable Sub OnValueChanged(ByVal e As ValueChangedEventArgs)
        ' Raise the ValueChanged event so applications can be alerted
        ' when Value changes.
        MyBase.RaiseEvent(e)
    End Sub


#Region "NUDCode"
    Private Sub UpdateStates(ByVal useTransitions As Boolean)

        If Value >= 0 Then
            VisualStateManager.GoToState(Me, "Positive", useTransitions)
        Else
            VisualStateManager.GoToState(Me, "Negative", useTransitions)
        End If

        If IsFocused Then
            VisualStateManager.GoToState(Me, "Focused", useTransitions)
        Else
            VisualStateManager.GoToState(Me, "Unfocused", useTransitions)

        End If
    End Sub

    Public Overloads Overrides Sub OnApplyTemplate()

        UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
        DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)

        UpdateStates(False)
    End Sub

    Private m_downButtonElement As RepeatButton

    Private Property DownButtonElement() As RepeatButton
        Get
            Return m_downButtonElement
        End Get

        Set(ByVal value As RepeatButton)

            If m_downButtonElement IsNot Nothing Then
                RemoveHandler m_downButtonElement.Click, AddressOf downButtonElement_Click
            End If
            m_downButtonElement = value

            If m_downButtonElement IsNot Nothing Then
                AddHandler m_downButtonElement.Click, AddressOf downButtonElement_Click
            End If
        End Set
    End Property

    Private Sub downButtonElement_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
        Value -= 1
    End Sub

    Private m_upButtonElement As RepeatButton

    Private Property UpButtonElement() As RepeatButton
        Get
            Return m_upButtonElement
        End Get

        Set(ByVal value As RepeatButton)
            If m_upButtonElement IsNot Nothing Then
                RemoveHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
            End If
            m_upButtonElement = value

            If m_upButtonElement IsNot Nothing Then
                AddHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
            End If
        End Set
    End Property

    Private Sub upButtonElement_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
        Value += 1
    End Sub

    Protected Overloads Overrides Sub OnMouseLeftButtonDown(ByVal e As MouseButtonEventArgs)
        MyBase.OnMouseLeftButtonDown(e)
        Focus()
    End Sub


    Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
        MyBase.OnGotFocus(e)
        UpdateStates(True)
    End Sub

    Protected Overloads Overrides Sub OnLostFocus(ByVal e As RoutedEventArgs)
        MyBase.OnLostFocus(e)
        UpdateStates(True)
    End Sub
#End Region
End Class


Public Delegate Sub ValueChangedEventHandler(ByVal sender As Object,
                                             ByVal e As ValueChangedEventArgs)

Public Class ValueChangedEventArgs
    Inherits RoutedEventArgs

    Public Sub New(ByVal id As RoutedEvent,
                   ByVal num As Integer)

        Value = num
        RoutedEvent = id
    End Sub

    Public ReadOnly Property Value() As Integer
End Class

Siehe auch