2014年5月17日 星期六

DependencyProperty

撰寫 Presentation Foundation 相關的框架時,免不了要自行製作一些 UserControl,而在製作 UserControl 時如果想要讓後續使用此控制項的開發者可以依循 MVVM 的方式操作時,替 UserControl 加入 DependencyProperty 就是一件很重要的工作。

此外值得一提,某些從規模較大的公司過來面試的資深工程師,他們提到公司內部在撰寫 UserControl 時,習慣將這些元件打包成 dll 或是 msi 的檔案,再將此 dll 或 msi 提供給其他工程師以匯入 Visual Studio 的 ToolBox 的方式,即可方便其他工程師以 Drag and drop 的方式使用這些元件。而其實在開啟專案時選擇 UserControl 專案,即可直接編譯出可供匯入 ToolBox 的檔案,在 Visual Studio 的 ToolBox 上點選右鍵,選擇工具即可匯入,如果還是提供元件程式碼給其他 member 的人,不妨試試這種方式讓新的專案更乾淨。

開始這次的紀錄的重點,假設今天我們做了一個 UserControl 是一個可以指定背景色,且可以輸入 Header 與 Text 的元件,最容易的方式可以這麼寫

XAML

<UserControl x:Class="WpfApplication1.MyUserControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Grid.Background>
            <SolidColorBrush Color="{Binding BackgroundColor}"/>
        </Grid.Background>
        <Grid.RowDefinitions>
            <RowDefinition Height="50"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <TextBlock Grid.Row="0"
                   Padding="10,10,10,0"
                   Text="{Binding Header}"
                   FontSize="30"
                   Foreground="#333333"/>
        <TextBlock Grid.Row="1"
                   Padding="10"
                   Text="{Binding ContentText}"
                   FontSize="18"
                   Foreground="#666666"
                   TextWrapping="Wrap"/>
    </Grid>
</UserControl>

C# Code

public partial class MyUserControl : UserControl, INotifyPropertyChanged
{
    public MyUserControl()
    {
        InitializeComponent();
        DataContext = this;
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] String propertyName = null)
    {
        if (object.Equals(storage, value))
        {
            return false;
        }

        storage = value;
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        return true;
    }

    private String backgroundColor;
    public String BackgroundColor
    {
        get
        {
            return backgroundColor;
        }
        set
        {
            SetProperty(ref backgroundColor, value, "BackgroundColor");
        }
    }

    private String contextText;
    public String ContentText
    {
        get
        {
            return contextText;
        }
        set
        {
            SetProperty(ref contextText, value, "ContentText");
        }
    }

    private String header;
    public String Header
    {
        get
        {
            return header;
        }
        set
        {
            SetProperty(ref header, value, "Header");
        }
    }
}

在使用這個 MyUserControl 時的方法如下

XAML

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication1"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel Margin="10">
        <local:MyUserControl Width="200" Height="120"
                             BackgroundColor="#00AED8"
                             Header="I'm Header 1"
                             ContentText="Hello World, this is my first control!"/>

        <local:MyUserControl Width="200" Height="120"
                             BackgroundColor="#FFA0A0"
                             Header="I'm Header 2"
                             ContentText="Hello World, this is my second control!"/>
    </StackPanel>
</Window>

如果使用這個元件的開發者想使用 MVVM 的方式來撰寫時,他可能會想把 Header 的值改成 {Binding Header1} 甚至將這兩些 local:MyUserControl 擺到 ListBox 中 Bind 一個 List 屬性,這時 XAML 內容在編譯時會被錯誤提示為 A 'Binding' can only be set on a DependencyProperty of a DependencyObject. 故以下示範將這個 MyUserControl 改寫為可 Binding 的元件

MyUserControl.xaml

<UserControl x:Class="WpfApplication1.MyUserControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d"
             x:Name="root"
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Grid.Background>
            <SolidColorBrush Color="{Binding BackgroundColor, ElementName=root}"/>
        </Grid.Background>
        <Grid.RowDefinitions>
            <RowDefinition Height="50"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <TextBlock Grid.Row="0"
                   Padding="10,10,10,0"
                   Text="{Binding Header, ElementName=root}"
                   FontSize="30"
                   Foreground="#333333"/>
        <TextBlock Grid.Row="1"
                   Padding="10"
                   Text="{Binding ContentText, ElementName=root}"
                   FontSize="18"
                   Foreground="#666666"
                   TextWrapping="Wrap"/>
    </Grid>
</UserControl>

MyUserControl.xaml.cs

public partial class MyUserControl : UserControl, INotifyPropertyChanged
{
    public MyUserControl()
    {
        InitializeComponent();
        //DataContext = this;
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] String propertyName = null)
    {
        if (object.Equals(storage, value))
        {
            return false;
        }

        storage = value;
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        return true;
    }

    private String backgroundColor;
    public String BackgroundColor
    {
        get
        {
            return backgroundColor;
        }
        set
        {
            SetProperty(ref backgroundColor, value, "BackgroundColor");
        }
    }

    public static readonly DependencyProperty ContentTextProperty =
    DependencyProperty.Register("ContentText",
    typeof(String), typeof(MyUserControl), null);
    public String ContentText
    {
        get
        {
            return (String)GetValue(ContentTextProperty);
        }
        set
        {
            SetValue(ContentTextProperty, value);
        }
    }

    public static readonly DependencyProperty HeaderProperty =
    DependencyProperty.Register("Header",
    typeof(String), typeof(MyUserControl), null);
    public String Header
    {
        get
        {
            return (String)GetValue(HeaderProperty);
        }
        set
        {
            SetValue(HeaderProperty, value);
        }
    }
}

使用 MyUserControl 的 MainWindow 即可使用 Binding 或原本固定的字串任一種方式,例如

MainWindow.xaml

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication1"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel Margin="10">
        <local:MyUserControl Width="200" Height="120"
                             BackgroundColor="#00AED8"
                             Header="{Binding Header1}"
                             ContentText="{Binding ContentText}"/>

        <local:MyUserControl Width="200" Height="120"
                             BackgroundColor="#FFA0A0"
                             Header="{Binding Header2}"
                             ContentText="Hello World, this is my second control!"/>
    </StackPanel>
</Window>

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = this;
    }

    public String Header1
    {
        get
        {
            return "Header 1";
        }
    }

    public String Header2
    {
        get
        {
            return "Header 2";
        }
    }

    public String ContentText
    {
        get
        {
            return "Hello World, this is my first control!";
        }
    }
}

如果改寫完成後發生 Binding 的資料沒有如期出現的狀況,檢查一下是否犯了以下幾種常見錯誤

1. Control 的 DenpendencyProperty 參數是否有給正確的值及 Property 名稱
2. Control 原先建構式中的 DataContext = this; 必須拿掉
3. Control 的 XAML 中是否有在需支援的屬性加上 ElementName=root 與 Control 本身是否命名為 root 或其他名稱

以上三個常犯錯誤的發生原因通常是對 Binding 的階層關係不夠熟悉,建議再將文件 ( Data Binding Overview ) 詳細的讀過,要進行更進階的操作如新增 Model 的階層時才有辦法撰寫出正確的結果。


沒有留言:

張貼留言