[WP8] Binding時,依照DataType選擇DataTemplate

本篇文章介紹如何在Binding時,依照資料類別的DataType選擇對應的DataTemplate,用以在Binding之後,於畫面上依照不同資料類別來呈現不同外觀。

[WP8] Binding時,依照DataType選擇DataTemplate

範例下載

範例程式碼:點此下載

問題情景

在開發WPF、WP8...這類應用程式的時候,透過Binding機制搭配DataTemplate,能讓資料類別在經過Binding之後於畫面上呈現。例如下列的範例,透過Binding機制搭配DataTemplate,就能在WP8的ListBox控制項中,依照DataTemplate的定義,來呈現Car物件集合。

  • 執行結果

    Sample001

  • 程式碼(.CS)

    namespace BindingSample001.Models
    {
        public class Car
        {
            public string Name { get; set; }
        }
    }
    
    namespace BindingSample001
    {
        public partial class MainPage : PhoneApplicationPage
        {
            // Constructors
            public MainPage()
            {
                // Initialize
                this.InitializeComponent();
    
                // Data
                var carList = new List<Car>();
                carList.Add(new Car() { Name = "C001" });
                carList.Add(new Car() { Name = "C002" });
                carList.Add(new Car() { Name = "C003" });
                carList.Add(new Car() { Name = "C004" });
                carList.Add(new Car() { Name = "C005" });
    
                // Binding
                this.ListBox001.ItemsSource = carList;
            }
        }
    }
    
  • 程式碼(.XAML)

    <!--Resources-->
    <phone:PhoneApplicationPage.Resources>
    
        <!--Car Template-->
        <DataTemplate x:Key="CarTemplate">
            <StackPanel Background="LightGreen"  Margin="12,6" FlowDirection="LeftToRight">
                <TextBlock Text="Car" />
                <CheckBox Content="{Binding Path=Name}" />
            </StackPanel>
        </DataTemplate>
    
    </phone:PhoneApplicationPage.Resources>
    
    
    <!--LayoutRoot-->
    <ListBox x:Name="ListBox001">
    
        <!--ItemTemplate-->
        <ListBox.ItemTemplate>
            <StaticResource ResourceKey="CarTemplate" />
        </ListBox.ItemTemplate>
    
        <!--Style-->
        <ListBox.ItemContainerStyle>
            <Style TargetType="ListBoxItem">
                <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
            </Style>
        </ListBox.ItemContainerStyle>
    
    </ListBox>
    

而在一些更複雜的開發專案中,畫面上不單單只需要呈現一種的資料類別,而是需要呈現資料類別的各種延伸資料類別,並且這些不同種類的延伸資料類別,在經過Binding之後於畫面上必須要有不同種類的呈現外觀。例如下列的範例,Car類別為基礎資料類別,Truck類別、Sedan類別各自為Car類別的延伸類別,這些Truck類別、Sedan類別在經過Binding之後,於畫面上Truck類別會有一種呈現外觀、而Sedan類別會有另外一種呈現外觀。

本篇文章介紹如何在Binding時,依照資料類別的DataType選擇對應的DataTemplate,用以在Binding之後,於畫面上依照不同資料類別來呈現不同外觀,為自己留個紀錄也希望能幫助到有需要的開發人員。

  • 類別結構

    Class001

  • 類別程式碼(.CS)

    public class Car
    {
        public string Name { get; set; }
    }       
    
    public class Truck : Car
    {
        public int MaxLoad { get; set; }
    }
    
    public class Sedan : Car
    {
        public int MaxSpeed { get; set; }
    }
    

在WPF中,依照DataType選擇DataTemplate

在WPF中,可以定義DataTemplate.DataType這個屬性,用來指定DataTemplate所對應的資料類別。當定義這個屬性之後,WPF應用程式在Binding時,就可以依照資料物件的DataType選擇DataTemplate,並且使用這個DataTemplate的定義來呈現資料物件。例如下列的範例,Truck類別、Sedan類別在經過Binding之後,於畫面上Truck類別會有一種呈現外觀、而Sedan類別會有另外一種呈現外觀。

  • 執行結果

    Sample002

  • 程式碼(.CS)

    namespace BindingSample002
    {
        public partial class MainWindow : Window
        {
            // Constructors
            public MainWindow()
            {
                // Initialize
                this.InitializeComponent();
    
                // Source
                var carList = new List<Car>();
                carList.Add(new Truck() { Name = "T001", MaxLoad = 100 });
                carList.Add(new Sedan() { Name = "S002", MaxSpeed = 200 });
                carList.Add(new Truck() { Name = "T003", MaxLoad = 300 });
                carList.Add(new Truck() { Name = "T004", MaxLoad = 400 });
                carList.Add(new Sedan() { Name = "S005", MaxSpeed = 500 });
    
                // Binding
                this.ListBox001.ItemsSource = carList;
            }
        }
    }
    
  • 程式碼(.XAML)

    <!--Resources-->
    <Window.Resources>
    
        <!--Truck Template-->
        <DataTemplate DataType="{x:Type models:Truck}">
            <StackPanel Background="LightBlue"  Margin="12,6" FlowDirection="LeftToRight">
                <TextBlock Text="Truck" />
                <CheckBox Content="{Binding Path=Name}" />
                <CheckBox Content="{Binding Path=MaxLoad}" />
            </StackPanel>
        </DataTemplate>
    
        <!--Sedan Template-->
        <DataTemplate DataType="{x:Type models:Sedan}">
            <StackPanel Background="LightPink" Margin="12,6" FlowDirection="RightToLeft">
                <TextBlock Text="Sedan" />
                <TextBlock Text="{Binding Path=Name}" />
                <TextBlock Text="{Binding Path=MaxSpeed}" />
            </StackPanel>
        </DataTemplate>
    
    </Window.Resources>
    
    
    <!--LayoutRoot-->
    <ListBox x:Name="ListBox001">
    
        <!--Style-->
        <ListBox.ItemContainerStyle>
            <Style TargetType="ListBoxItem">
                <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
            </Style>
        </ListBox.ItemContainerStyle>
    
    </ListBox>
    

在WP8中,依照DataType選擇DataTemplate

因為一些原因,在WP8中目前沒有提供DataTemplate.DataType這個屬性,用來指定DataTemplate所對應的資料類別,這也讓WP8應用程式,沒有辦法提供在Binding之後,於畫面上依照不同資料類別來呈現不同外觀的這個功能。但是山不轉路轉,開發人員還是可以透過Binding機制、結合自訂的IValueConverter,來提供依照DataType選擇DataTemplate這個功能。

  • 程式碼(.XAML)

    Error001

首先在頁面中使用下列的XAML宣告來取代原有的DataTemplate,讓DataTemplate的選擇機制,改為使用Binding以及自訂的TypeTemplateConverter來提供。

  • 程式碼(.XAML)

    <!--LayoutRoot-->
    <ListBox x:Name="ListBox001">
    
        <!--ItemTemplate-->
        <ListBox.ItemTemplate>
            <DataTemplate>
                <ContentControl Content="{Binding}" ContentTemplate="{Binding Converter={StaticResource TypeTemplateConverter}}" HorizontalContentAlignment="Stretch" />
            </DataTemplate>
        </ListBox.ItemTemplate>
    
        <!--Style-->
        <ListBox.ItemContainerStyle>
            <Style TargetType="ListBoxItem">
                <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
            </Style>
        </ListBox.ItemContainerStyle>
    
    </ListBox>
    

接著就是依照下列的程式碼來建立自訂的TypeTemplateConverter。這個自訂的TypeTemplateConverter實作IValueConverter,用來處理Binding的目標資料物件,並且依照目標資料物件的型別來提供DataTemplate。

這個設計直覺上會認為沒有問題,但實際撰寫這個Converter的時候會發現,接收目標資料物件、取得目標資料物件型別這些功能實作都沒有問題,但是如何取得DataTemplate卻是一個問題(範例中[???]部分的程式碼)。在TypeTemplateConverter並沒有定義DataTemplate的來源,沒有來源就沒有辦法取得DataTemplate,那當然也就沒有辦法依照目標資料物件型別來提供DataTemplate。

  • 程式碼(.CS)

    public sealed class TypeTemplateConverter : IValueConverter
    {
        // Methods 
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            // Require
            if (value == null) return null;
    
            // TypeName
            string typeName = value.GetType().ToString();
    
            // DataTemplate
            DataTemplate dataTemplate = [???];
            if (dataTemplate == null) return null;
    
            // Convert
            return dataTemplate;
        }
    
        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
    

這時為了提供DataTemplate的來源,開發人員可以為TypeTemplateConverter類別加入System.Windows.FrameworkElement類別的繼承,這樣就可以使用FrameworkElement的Resources屬性做為DataTemplate的來源。在這之後TypeTemplateConverter就可以使用目標資料物件型別作為索引,取得Resources屬性之中的DataTemplate,用以提供Binding機制使用。

  • 程式碼(.CS)

    namespace BindingSample003.Converters
    {
        public sealed class TypeTemplateConverter : System.Windows.FrameworkElement, IValueConverter
        {
            // Methods 
            public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
            {
                // Require
                if (value == null) return null;
    
                // TypeName
                string typeName = value.GetType().ToString();
    
                // DataTemplate
                var dataTemplate = this.Resources[typeName] as DataTemplate;
                if (dataTemplate == null) return null;
    
                // Convert
                return dataTemplate;
            }
    
            public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
            {
                throw new NotImplementedException();
            }
        }
    }
    

為TypeTemplateConverter類別加入System.Windows.FrameworkElement類別的繼承之後,在XAML定義中就可以使用XAML語法來定義TypeTemplateConverter物件所要提供的DataTemplate。

  • 程式碼(.XAML)

    <!--Resources-->
    <phone:PhoneApplicationPage.Resources>
    
        <!--Converter-->
        <converters:TypeTemplateConverter x:Key="TypeTemplateConverter" >
            <converters:TypeTemplateConverter.Resources>
    
                <!--Truck Template-->
                <DataTemplate x:Key="BindingSample003.Models.Truck">
                    <StackPanel Background="LightBlue"  Margin="12,6" FlowDirection="LeftToRight">
                        <TextBlock Text="Truck" />
                        <CheckBox Content="{Binding Path=Name}" />
                        <CheckBox Content="{Binding Path=MaxLoad}" />
                    </StackPanel>
                </DataTemplate>
    
                <!--Sedan Template-->
                <DataTemplate x:Key="BindingSample003.Models.Sedan">
                    <StackPanel Background="LightPink" Margin="12,6" FlowDirection="RightToLeft">
                        <TextBlock Text="Sedan" />
                        <TextBlock Text="{Binding Path=Name}" />
                        <TextBlock Text="{Binding Path=MaxSpeed}" />
                    </StackPanel>
                </DataTemplate>
    
            </converters:TypeTemplateConverter.Resources>
        </converters:TypeTemplateConverter>
    
    </phone:PhoneApplicationPage.Resources>
    

完成上列的設計與定義之後,透過Binding機制、結合自訂的TypeTemplateConverter,WP8應用程式在Binding時,就可以依照資料物件的DataType選擇DataTemplate,並且使用這個DataTemplate的定義來呈現資料物件。例如下列的範例,Truck類別、Sedan類別在經過Binding之後,於畫面上Truck類別會有一種呈現外觀、而Sedan類別會有另外一種呈現外觀。

  • 執行結果

    Sample003

  • 程式碼(.CS)

    namespace BindingSample003
    {
        public partial class MainPage : PhoneApplicationPage
        {
            // Constructors
            public MainPage()
            {
                // Initialize
                this.InitializeComponent();
    
                // Data
                var carList = new List<Car>();
                carList.Add(new Truck() { Name = "T001", MaxLoad = 100 });
                carList.Add(new Sedan() { Name = "S002", MaxSpeed = 200 });
                carList.Add(new Truck() { Name = "T003", MaxLoad = 300 });
                carList.Add(new Truck() { Name = "T004", MaxLoad = 400 });
                carList.Add(new Sedan() { Name = "S005", MaxSpeed = 500 });
    
                // Binding
                this.ListBox001.ItemsSource = carList;
            }
        }
    }
    
  • 程式碼(.XAML)

    <!--Resources-->
    <phone:PhoneApplicationPage.Resources>
    
        <!--Converter-->
        <converters:TypeTemplateConverter x:Key="TypeTemplateConverter" >
            <converters:TypeTemplateConverter.Resources>
    
                <!--Truck Template-->
                <DataTemplate x:Key="BindingSample003.Models.Truck">
                    <StackPanel Background="LightBlue"  Margin="12,6" FlowDirection="LeftToRight">
                        <TextBlock Text="Truck" />
                        <CheckBox Content="{Binding Path=Name}" />
                        <CheckBox Content="{Binding Path=MaxLoad}" />
                    </StackPanel>
                </DataTemplate>
    
                <!--Sedan Template-->
                <DataTemplate x:Key="BindingSample003.Models.Sedan">
                    <StackPanel Background="LightPink" Margin="12,6" FlowDirection="RightToLeft">
                        <TextBlock Text="Sedan" />
                        <TextBlock Text="{Binding Path=Name}" />
                        <TextBlock Text="{Binding Path=MaxSpeed}" />
                    </StackPanel>
                </DataTemplate>
    
            </converters:TypeTemplateConverter.Resources>
        </converters:TypeTemplateConverter>
    
    </phone:PhoneApplicationPage.Resources>
    
    
    <!--LayoutRoot-->
    <ListBox x:Name="ListBox001">
    
        <!--ItemTemplate-->
        <ListBox.ItemTemplate>
            <DataTemplate>
                <ContentControl Content="{Binding}" ContentTemplate="{Binding Converter={StaticResource TypeTemplateConverter}}" HorizontalContentAlignment="Stretch" />
            </DataTemplate>
        </ListBox.ItemTemplate>
    
        <!--Style-->
        <ListBox.ItemContainerStyle>
            <Style TargetType="ListBoxItem">
                <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
            </Style>
        </ListBox.ItemContainerStyle>
    
    </ListBox>
    
期許自己
能以更簡潔的文字與程式碼,傳達出程式設計背後的精神。
真正做到「以形寫神」的境界。