Bagikan melalui


Membuat Kontrol yang Memiliki Tampilan yang Dapat Disesuaikan

Windows Presentation Foundation (WPF) memberi Anda kemampuan untuk membuat kontrol yang tampilannya dapat disesuaikan. Misalnya, Anda dapat mengubah tampilan CheckBox lebih dari yang dapat dilakukan dengan mengatur properti dengan membuat ControlTemplate baru. Ilustrasi berikut menunjukkan CheckBox yang menggunakan default ControlTemplate dan CheckBox yang menggunakan kustom ControlTemplate.

Kotak centang dengan templat kontrol default. Kotak Centang yang menggunakan templat kontrol default

Kotak centang dengan templat kontrol kustom. Kotak Centang yang menggunakan templat kontrol kustom

Jika Anda mengikuti model bagian dan status saat membuat kontrol, tampilan kontrol Anda akan dapat disesuaikan. Alat desainer seperti Blend for Visual Studio mendukung model bagian dan status, jadi ketika Anda mengikuti model ini, kontrol Anda akan dapat disesuaikan dalam jenis aplikasi tersebut. Topik ini membahas model bagian dan status dan cara mengikutinya saat Anda membuat kontrol Anda sendiri. Topik ini menggunakan contoh kontrol kustom, NumericUpDown, untuk mengilustrasikan filosofi model ini. NumericUpDown Kontrol menampilkan nilai numerik, yang dapat ditingkatkan atau dikurangi pengguna dengan mengklik tombol kontrol. Ilustrasi berikut menunjukkan kontrol NumericUpDown yang dibahas dalam topik ini.

Kontrol kustom NumericUpDown. Kontrol NumericUpDown kustom

Topik ini berisi bagian berikut:

Prasyarat

Topik ini mengasumsikan bahwa Anda tahu cara membuat ControlTemplate baru untuk kontrol yang sudah ada, memahami unsur-unsur dalam kontrak kontrol, dan memahami konsep yang dijelaskan di Membuat template untuk kontrol.

Nota

Untuk membuat kontrol yang dapat menyesuaikan tampilannya, Anda harus membuat kontrol yang mewarisi dari Control kelas atau salah satu subkelasnya selain UserControl. Kontrol yang mewarisi adalah UserControl kontrol yang dapat dibuat dengan cepat, tetapi tidak menggunakan ControlTemplate dan Anda tidak dapat menyesuaikan tampilannya.

Model Bagian dan Status

Model bagian dan status menentukan cara menentukan struktur visual dan perilaku visual kontrol. Untuk mengikuti model komponen dan keadaan, Anda perlu melakukan langkah-langkah berikut:

  • Tentukan struktur visual dan perilaku visual dalam ControlTemplate kontrol.

  • Ikuti praktik terbaik tertentu saat logika kontrol Anda berinteraksi dengan bagian templat kontrol.

  • Berikan kontrak kontrol untuk menentukan apa yang harus disertakan ControlTemplatedalam .

Saat Anda menentukan struktur visual dan perilaku visual dalam ControlTemplate kontrol, penulis aplikasi dapat mengubah struktur visual dan perilaku visual kontrol Anda dengan membuat kode baru ControlTemplate alih-alih menulis. Anda harus memberikan kontrak kontrol yang memberi tahu penulis aplikasi objek dan status mana yang FrameworkElement harus didefinisikan dalam ControlTemplate. Anda harus mengikuti beberapa praktik terbaik saat berinteraksi dengan bagian-bagian dalam ControlTemplate sehingga kontrol Anda menangani ControlTemplate yang tidak lengkap dengan benar. Jika Anda mengikuti ketiga prinsip ini, penulis aplikasi akan dapat membuat ControlTemplate untuk kontrol Anda semudah mereka membuatnya untuk kontrol yang disertakan dengan WPF. Bagian berikut menjelaskan masing-masing rekomendasi ini secara rinci.

Menentukan Struktur Visual dan Perilaku Visual Kontrol dalam ControlTemplate

Saat Anda membuat kontrol kustom dengan menggunakan model bagian dan status, Anda menentukan struktur visual kontrol dan perilaku visual di dalamnya ControlTemplate alih-alih dalam logikanya. Struktur visual kontrol adalah komposit FrameworkElement objek yang membentuk kontrol. Perilaku visual adalah cara kontrol muncul ketika berada dalam keadaan tertentu. Untuk informasi lebih lanjut tentang cara membuat ControlTemplate yang menentukan struktur visual dan perilaku visual dari kontrol, lihat Membuat templat untuk kontrol.

Dalam contoh kontrol NumericUpDown, struktur visual mencakup dua kontrol RepeatButton dan satu TextBlock. Jika Anda menambahkan kontrol ini dalam kode NumericUpDown kontrol--di konstruktornya, misalnya--posisi kontrol tersebut tidak akan dapat diubah. Alih-alih mendefinisikan struktur visual kontrol dan perilaku visual dalam kodenya, Anda harus menentukannya di ControlTemplate. Kemudian pengembang aplikasi untuk menyesuaikan posisi tombol dan TextBlock menentukan perilaku apa yang terjadi ketika Value negatif karena ControlTemplate dapat diganti.

Contoh berikut menunjukkan struktur visual dari kontrol NumericUpDown, yang mencakup RepeatButton untuk meningkatkan Value, sebuah RepeatButton untuk mengurangi Value, dan TextBlock untuk menampilkan Value.

<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>

Perilaku visual dari kontrol NumericUpDown adalah nilai tersebut berwarna merah jika negatif. Jika Anda mengubah Foreground dari TextBlock dalam kode saat Value negatif, NumericUpDown akan selalu menampilkan nilai negatif merah. Anda menentukan perilaku visual kontrol di ControlTemplate dengan menambahkan VisualState objek ke ControlTemplate. Contoh berikut menunjukkan VisualState objek untuk status Positive dan Negative . Positive dan Negative saling eksklusif (kontrol selalu tepat di salah satu dari keduanya), sehingga contohnya menempatkan VisualState objek ke dalam satu VisualStateGroup. Ketika sistem masuk ke Negative status, Foreground dari TextBlock menjadi merah. Saat kontrol berada dalam status Positive , kembali Foreground ke nilai aslinya. Menentukan VisualState objek dalam ControlTemplate lebih lanjut dibahas dalam Membuat templat untuk kontrol.

Nota

Pastikan untuk mengatur properti terlampir VisualStateManager.VisualStateGroups dari akar FrameworkElementControlTemplate.

<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>

Menggunakan Bagian Template Kontrol dalam Kode

Penulis ControlTemplate mungkin menghilangkan FrameworkElement atau VisualState objek, baik secara sengaja atau tidak sengaja, tetapi logika kontrol Anda mungkin memerlukan bagian-bagian tersebut agar berfungsi dengan baik. Model bagian dan status menentukan bahwa kontrol Anda harus tangguh terhadap ControlTemplate yang kehilangan objek FrameworkElement atau VisualState. Kontrol Anda tidak boleh melempar pengecualian atau melaporkan kesalahan jika FrameworkElement, VisualState, atau VisualStateGroup hilang dari ControlTemplate. Bagian ini menjelaskan praktik yang direkomendasikan untuk berinteraksi dengan FrameworkElement objek dan mengelola status.

Mengantisipasi Objek FrameworkElement yang Hilang

Saat Anda menentukan FrameworkElement objek dalam ControlTemplate, logika kontrol Anda mungkin perlu berinteraksi dengan beberapa di antaranya. Misalnya, kontrol NumericUpDown mendaftar ke acara tombol Click untuk menambah atau mengurangi Value dan menetapkan properti Text dari TextBlock ke Value. Jika kontrol kustom ControlTemplate menghilangkan TextBlock atau tombol, dapat diterima bahwa kontrol kehilangan beberapa fungsionalitasnya, namun Anda harus memastikan bahwa kontrol Anda tidak menimbulkan kesalahan. Misalnya, jika ControlTemplate tidak berisi tombol untuk mengubah Value, maka NumericUpDown kehilangan fungsionalitas tersebut, tetapi aplikasi yang menggunakan ControlTemplate akan terus berjalan.

Praktik berikut akan memastikan bahwa kontrol Anda merespons objek yang hilang FrameworkElement dengan benar:

  1. Atur x:Name atribut untuk setiap FrameworkElement yang perlu Anda referensikan dalam kode.

  2. Tentukan properti privat untuk masing-masing FrameworkElement yang perlu Anda berinteraksi dengan.

  3. Berlangganan atau berhenti berlangganan dari event apa pun yang ditangani kontrol Anda dalam pengatur akses properti FrameworkElement.

  4. Atur properti FrameworkElement yang Anda tentukan di langkah 2 dengan menggunakan metode OnApplyTemplate. Ini adalah waktu terawal FrameworkElement dalam ControlTemplate dapat dikendalikan. Gunakan x:Name milik FrameworkElement untuk mendapatkannya dari ControlTemplate.

  5. Pastikan bahwa FrameworkElement tidak null sebelum mengakses anggota-anggotanya. Jika ya null, jangan laporkan kesalahan.

Contoh berikut menunjukkan bagaimana NumericUpDown kontrol berinteraksi dengan FrameworkElement objek sesuai dengan rekomendasi dalam daftar sebelumnya.

Dalam contoh yang mendefinisikan struktur visual NumericUpDown dari kontrol di ControlTemplate, RepeatButton yang meningkatkan Value memiliki atribut x:Name yang diatur ke UpButton. Contoh berikut mendeklarasikan properti yang disebut UpButtonElement yang mewakili RepeatButton yang dideklarasikan dalam ControlTemplate. Aksesori set pertama-tama membatalkan langganan dari peristiwa tombol Click jika UpDownElement tidak null, lalu mengatur properti, dan kemudian berlangganan ke peristiwa Click. Ada juga properti yang ditentukan, tetapi tidak ditampilkan di sini, untuk RepeatButton lainnya, yang disebut 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

Contoh berikut menunjukkan OnApplyTemplate untuk NumericUpDown kontrol. Contoh menggunakan GetTemplateChild metode untuk mendapatkan FrameworkElement objek dari ControlTemplate. Perhatikan bahwa contoh melindungi dari kasus di mana GetTemplateChild menemukan FrameworkElement dengan nama yang ditentukan yang bukan dari jenis yang diharapkan. Ini juga merupakan praktik terbaik untuk mengabaikan elemen yang memiliki jenis yang ditentukan x:Name tetapi merupakan jenis yang salah.

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

Dengan mengikuti praktik yang ditampilkan dalam contoh sebelumnya, Anda memastikan bahwa kontrol Anda akan terus berjalan ketika ControlTemplate hilang .FrameworkElement

Menggunakan VisualStateManager untuk Mengelola Status

VisualStateManager melacak status kontrol dan melakukan logika yang diperlukan untuk transisi antar status. Saat Anda menambahkan VisualState objek ke ControlTemplate, Anda menambahkannya ke VisualStateGroup dan menambahkannya VisualStateGroup ke VisualStateManager.VisualStateGroups properti terlampir sehingga VisualStateManager memiliki akses ke objek tersebut.

Contoh berikut mengulangi contoh sebelumnya yang memperlihatkan objek VisualState yang sesuai dengan keadaan Positive dan Negative pada kontrol. Storyboard dalam NegativeVisualState mengubah Foreground dari TextBlock menjadi merah. Ketika pengendali berada dalam keadaan NumericUpDown, papan cerita dalam keadaan Negative mulai berjalan. Kemudian Storyboard dalam status Negative berhenti ketika kontrol kembali ke status Positive. Positive VisualState tidak perlu berisi Storyboard karena ketika Storyboard untuk Negative berhenti, Foreground kembali ke warna aslinya.

<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>

Perhatikan bahwa TextBlock diberikan nama, tetapi TextBlock tidak ada dalam kontrak kontrol karena NumericUpDown logika kontrol tidak pernah mereferensikan TextBlock. Elemen yang dirujuk dalam ControlTemplate memiliki nama, tetapi tidak perlu menjadi bagian dari kontrak kontrol karena baru ControlTemplate untuk kontrol mungkin tidak perlu mereferensikan elemen tersebut. Misalnya, seseorang yang membuat satu ControlTemplate baru untuk NumericUpDown mungkin memutuskan untuk tidak menunjukkan bahwa Value negatif dengan mengubah Foreground. Dalam hal ini, baik kode maupun ControlTemplate tidak merujuk TextBlock berdasarkan nama.

Logika kontrol bertanggung jawab untuk mengubah status kontrol. Contoh berikut menunjukkan bahwa NumericUpDown kontrol memanggil GoToState metode untuk masuk ke Positive status ketika Value 0 atau lebih besar, dan Negative status ketika Value kurang dari 0.

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

Metode GoToState melakukan logika yang diperlukan untuk memulai dan menghentikan storyboard dengan tepat. Saat kontrol memanggil GoToState untuk mengubah statusnya, VisualStateManager lakukan hal berikut:

  • VisualState Jika kontrol akan memiliki Storyboard, papan cerita dimulai. Kemudian, jika VisualState kontrol berasal dari memiliki Storyboard, papan cerita berakhir.

  • Jika kontrol sudah dalam status yang ditentukan, GoToState tidak mengambil tindakan dan mengembalikan true.

  • Jika status yang ditentukan tidak ada di ControlTemplate , controlGoToState tidak mengambil tindakan dan mengembalikan false.

Praktik Terbaik untuk Bekerja dengan VisualStateManager

Disarankan agar Anda melakukan hal berikut untuk mempertahankan status kontrol Anda:

  • Gunakan properti untuk melacak statusnya.

  • Buat metode pembantu untuk transisi antar status.

Kontrol NumericUpDown menggunakan propertinya Value untuk melacak apakah properti berada dalam status Positive atau Negative . NumericUpDown Kontrol juga mendefinisikan Focused dan status UnFocused, yang melacak properti IsFocused. Jika Anda menggunakan status yang tidak secara alami sesuai dengan properti kontrol, Anda dapat menentukan properti privat untuk melacak status.

Satu metode yang memperbarui semua status dapat memusatkan panggilan ke VisualStateManager dan menjaga agar kode Anda tetap mudah dikelola. Contoh berikut menunjukkan metode pembantu NumericUpDown kontrol, UpdateStates. Ketika Value lebih besar dari atau sama dengan 0, Control berada dalam status Positive . Ketika Value kurang dari 0, kontrol berada dalam status Negative . Ketika IsFocused adalah true, kontrol berada dalam keadaan Focused; jika tidak, berada dalam keadaan Unfocused. Kontrol dapat memanggil UpdateStates setiap kali perlu mengubah statusnya, terlepas dari perubahan status apa.

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

Jika Anda meneruskan nama status ke GoToState saat kontrol sudah dalam status tersebut, GoToState tidak melakukan apa pun, sehingga Anda tidak perlu memeriksa status kontrol saat ini. Misalnya, jika Value perubahan dari satu angka negatif ke angka negatif lainnya, papan cerita untuk Negative status tidak terganggu dan pengguna tidak akan melihat perubahan dalam kontrol.

VisualStateManager menggunakan VisualStateGroup objek untuk menentukan status mana yang akan keluar saat Anda memanggil GoToState. Kontrol selalu berada dalam satu status untuk setiap VisualStateGroup yang didefinisikan dalam ControlTemplate dan hanya meninggalkan status ketika masuk ke status lain dari VisualStateGroup yang sama. Misalnya, ControlTemplate kontrol mendefinisikan NumericUpDown objek danPositiveNegativedalam satu VisualState dan VisualStateGroup objek dan FocusedUnfocused di objek VisualState lainnya. (Anda dapat melihat Focused danUnfocusedVisualStatedidefinisikan di bagian Contoh Lengkap dalam topik ini Ketika kontrol beralih dari Positive status ke Negative status, atau sebaliknya, kontrol tetap berada dalam status Focused atau Unfocused .

Ada tiga tempat umum di mana status kontrol mungkin berubah:

Contoh berikut menunjukkan cara memperbarui status kontrol NumericUpDown pada kasus-kasus ini.

Anda harus memperbarui status kontrol dalam OnApplyTemplate metode sehingga kontrol muncul dalam status yang benar saat ControlTemplate diterapkan. Contoh berikut memanggil UpdateStatesOnApplyTemplate untuk memastikan bahwa kontrol berada dalam status yang sesuai. Misalnya, Anda membuat NumericUpDown kontrol, lalu mengaturnya Foreground ke hijau dan Value ke -5. Jika Anda tidak memanggil UpdateStates ketika ControlTemplate diterapkan ke kontrol NumericUpDown, kontrol tidak dalam status Negative dan nilainya hijau, bukan merah. Anda harus memanggil UpdateStates untuk menempatkan kontrol dalam status Negative .

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

Anda sering kali perlu memperbarui status kontrol saat properti berubah. Contoh berikut menunjukkan seluruh ValueChangedCallback metode. Karena ValueChangedCallback dipanggil ketika Value berubah, metode UpdateStates dipanggil jika Value berubah dari positif menjadi negatif atau sebaliknya. Dapat diterima untuk memanggil UpdateStates ketika Value berubah tetapi tetap positif atau negatif karena dalam hal ini, kontrol tidak akan mengubah keadaan.

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

Anda mungkin juga perlu memperbarui status saat peristiwa terjadi. Contoh berikut menunjukkan bahwa NumericUpDown memanggil UpdateStates pada Control untuk menangani kejadian GotFocus.

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

Kontrol VisualStateManager membantu Anda mengelola status. Dengan menggunakan VisualStateManager, Anda memastikan bahwa kontrol Anda bertransisi dengan benar antar status. Jika Anda mengikuti rekomendasi yang dijelaskan di bagian ini untuk bekerja dengan VisualStateManager, kode kontrol Anda akan tetap dapat dibaca dan dipertahankan.

Menyediakan Kontrak Pengendalian

Anda memberikan kontrak kontrol sehingga ControlTemplate penulis akan tahu apa yang harus dimasukkan ke dalam templat. Kontrak kontrol memiliki tiga elemen:

  • Elemen visual yang digunakan logika kontrol.

  • Status kontrol dan grup tempat setiap status berada.

  • Properti publik yang secara visual memengaruhi kontrol.

Seseorang yang menghasilkan ControlTemplate baru perlu mengetahui objek apa saja yang digunakan oleh logika kontrol FrameworkElement, jenis dari setiap objek tersebut, dan apa namanya. Penulis ControlTemplate juga perlu mengetahui nama setiap status yang mungkin dari kontrol, dan di mana VisualStateGroup status tersebut berada.

Kembali ke NumericUpDown contoh, kontrol mengharapkan ControlTemplate untuk memiliki objek berikut FrameworkElement :

Kontrol dapat berada dalam status berikut:

Untuk menentukan objek apa yang FrameworkElement diharapkan kontrol, Anda menggunakan TemplatePartAttribute, yang menentukan nama dan jenis elemen yang diharapkan. Untuk menentukan kemungkinan status kontrol, Anda menggunakan TemplateVisualStateAttribute, yang menentukan nama status dan miliknya VisualStateGroup . Letakkan TemplatePartAttribute dan TemplateVisualStateAttribute pada definisi kelas kontrol.

Properti publik apa pun yang memengaruhi tampilan kontrol Anda juga merupakan bagian dari kontrak kontrol.

Contoh berikut menentukan FrameworkElement objek dan status untuk NumericUpDown kontrol.

[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

Contoh Lengkap

Contoh berikut adalah keseluruhan kontrol ControlTemplate untuk NumericUpDown.

<!--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>

Contoh berikut menunjukkan logika untuk NumericUpDown.

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

Lihat juga