65.9K
CodeProject 正在变化。 阅读更多。
Home

在 WP7 中按类型动态应用数据模板

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2010 年 9 月 27 日

CPOL

9分钟阅读

viewsIcon

88915

downloadIcon

2136

本文介绍了一种在 Windows Phone 7 平台上按类型动态应用数据模板的方法。

引言

本文将介绍一种在Windows Phone 7平台上按类型动态应用数据模板的方法。文章将阐述WP7中数据模板的现有不便之处,然后提出解决方案。该解决方案还利用了MVVM模式提供的优势。示例中使用的MVVM框架是MVVM Light。

介绍数据模板

数据模板是一段XAML标记,用于指定数据绑定对象在UI中的显示方式。在WPF、Silverlight和WP7中,数据模板可以应用于两种不同类型的控件。它们可以通过ContentTemplate属性应用于内容控件,或者通过ItemTemplate属性应用于项控件。对于项控件,数据模板将用于显示已通过ItemsSource属性提供的元素集合中的每个项。

例如,如果我们有一个想要在UI ListBox控件中显示的string列表,我们只需要设置该控件的ItemsSource属性。ListBox会自动知道如何显示string,我们就能得到想要的结果。这方面的XAML代码可以在下面的列表中看到。

 <Grid Grid.Row="1" x:Name="ContentGrid">
       <ListBox ItemsSource="{Binding Path=Items}"
                ></ListBox>
 </Grid> 

上面的代码在手机上运行时会产生以下效果。

normalList.png

如果有一个复杂对象列表,并且需要显示其中每个对象的某些属性,情况会变得复杂一些。这时数据模板就非常有用。假设我们有一个想要显示的Customer对象列表。还假设我们想为每个Customer显示名字和姓氏。要做到这一点,我们需要一个数据模板。下面列表中的是我们将会使用的数据模板。

 <phone:PhoneApplicationPage.Resources>
        <DataTemplate x:Key="custTemplate">
            <StackPanel >
                <TextBlock Text="{Binding LastName}"
                           FontSize="30" Foreground="Red"></TextBlock>
                <TextBlock Text="{Binding FirstName}"
                           FontSize="25" Foreground="Green"
                           Margin="10,0,0,0"></TextBlock>
            </StackPanel>
        </DataTemplate>
    </phone:PhoneApplicationPage.Resources>

在上面的列表中首先要注意的是,数据模板被添加为页面资源。另一个要注意的是,这个数据模板指定姓氏将显示在名字上方,并且将使用更大的字体大小和不同的颜色显示。正如我之前所说,对于项控件,数据模板将应用于项控件的ItemTemplate属性。这可以在下面的列表中看到。

<ListBox ItemsSource="{Binding Path=Items}" Grid.Row="1" Margin="5"
                 ItemTemplate="{StaticResource custTemplate}">
        </ListBox> 

这段代码的效果可以在下图看到

simpleDataTemplate.png

WPF与WP7中的数据模板

与相关平台(WPF和Silverlight)不同,WP7中的数据模板非常有限。在WPF和Silverlight中,数据模板非常灵活。它们可以就地定义或作为资源定义。它们也可以手动或按类型自动应用。在这些平台上,数据模板还支持触发器。下面列表中的是一个为WPF编写的相对复杂的数据模板示例。

<Window.Resources>
   <DataTemplate DataType="{x:Type loc:Employee}">
     <StackPanel>
      <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding LastName}"/>
        <TextBlock Text="{Binding FirstName}"/>                                           
</StackPanel>
<TextBlock Text="{Binding Path=Salary}" x:Name="sal">
</TextBlock>
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=Salary}"  Value="90000">
<Setter TargetName="sal" Property="Foreground" Value="Red"/>
<Setter TargetName="sal" Property="FontWeight" Value="Bold"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>    
</Window.Resources> 

首先要注意的是,数据模板被添加为资源。这允许数据模板被定义一次并在多个地方使用。其次要注意的是,模板只设置了DataType属性。这里没有任何关于Key属性的内容。这使得框架可以在任何需要显示其定义的对象的场合自动应用该模板。下面的列表展示了将用于显示元素的ListBox

<ListBox ItemsSource="{Binding Path=Items}"
                 HorizontalContentAlignment="Stretch"
                 ></ListBox>

正如你所见,这里没有提到数据模板。数据模板将被自动应用。这可以在下图看到。你可以看到只有薪水为9000的条目具有红色前景。

complexDataTemplate.png

在WP7平台上,数据模板没有那么先进。在WP7中,我们可以就地定义数据模板或将其作为资源定义。但这仅限于此。没有触发器,也没有按类型自动应用数据模板的可能性。这意味着在将数据模板添加为资源时,我们必须指定x:Key属性。下面列表中的是一个这样的数据模板示例。

 <phone:PhoneApplicationPage.Resources>
        <DataTemplate x:Key="custTemplate">
            <StackPanel>
                <TextBlock Text="{Binding LastName}"/>
                <TextBlock Text="{Binding FirstName}"
                           Margin="10,0,0,0"/>
            </StackPanel>
        </DataTemplate>
    </phone:PhoneApplicationPage.Resources> 

要将此模板应用于列表框,我们需要使用StaticResource标记扩展来设置ItemTemplate属性。这可以在下面的列表中看到。

<ListBox ItemsSource="{Binding Path=Items}"
 ItemTemplate="{StaticResource custTemplate}">
</ListBox> 

问题

WP7数据模板中选项的缺乏严重限制了开发者的能力。在实际应用中,经常需要在同一个控件(无论是内容控件还是项控件)中显示不同类型的数据。由于运行时选择要显示的数据,无法动态更改数据模板就成了一个问题。例如,假设我们有一个选项列表,根据选定的选项,将在另一个控件中显示一个不同的对象。这显然是一个动态应用数据模板可以立即解决的问题。但是由于WP7不原生支持数据模板的这个选项,这种情况有点难以解决。

解决方案

解决数据模板问题的办法是找到一种方法,根据所选项目的类型返回一个数据模板,并将该数据模板应用于将要显示该项目的控件。数据模板是通过DataTemplateSelector类检索的。这是一个static类,有一个单一的static方法。该类的代码可以在下面的列表中看到。

 public static class DataTemplateSelector
    {
        public static DataTemplate GetTemplate(ViewModelBase param)
        {
            Type t = param.GetType();
            return App.Current.Resources[t.Name] as DataTemplate;
        }
    } 

从上面的代码可以看出,有一个单一的static方法。该方法将选定的项目作为参数,检索项目类型,并根据类型名称返回相应的数据模板。该解决方案还利用了约定优于配置的设计范例。这里唯一的约定是,数据模板的键必须与它们需要显示的类型的类型名称相同。下面列表中的是一个例子。

 <DataTemplate x:Key="FirstViewModel">
            <v:FirstView></v:FirstView>
        </DataTemplate> 

正如你所见,这个数据模板的键是“FirstViewModel”。这意味着这个数据模板将用于显示FirstViewModel类型的视图模型。这就是GetTemplate()方法所做的事情。根据参数的类型,它返回一个键与类型名称相同的`DataTemplate`。在上面的列表中,FirstView是用于显示的`UserControl`。

此方法检索到的数据模板将与将用于显示对象的ContentControlContentTemplate属性进行数据绑定。这可以在下面的列表中看到。

 <ContentControl Content="{Binding SelectedItem}"
                            ContentTemplate="{Binding Path=SelectedTemplate}"
                                HorizontalContentAlignment="Stretch"
                                VerticalContentAlignment="Stretch"></ContentControl> 

在上面的列表中,您可以看到内容和模板都绑定到视图模型中的属性。SelectedItem属性表示当前选择的、需要显示的项目,而SelectedTemplate是使用GetTemplate()方法检索到的相应数据模板。暴露数据模板的这个属性可以在下面的列表中看到。

 public DataTemplate SelectedTemplate
        {
            get 
            {
                if (selItem == null)
                    return null;
                return DataTemplateSelector.GetTemplate(selItem);
            }
        } 

尽管在大多数情况下将UI相关的代码放在ViewModel中不是个好主意,但我认为在这种情况下是允许的。这是因为属性中没有实际的UI内容。模板是在XAML中定义的,而且该属性实际上也没有使用与特定视图相关的UI代码。它使用了一个通用的数据模板。最后一件事要看的是,数据模板是如何根据选定的项目而变化的。这由SelectedItem属性解决。该属性的代码可以在下面的列表中看到。

 SelectableViewModel selItem;
        public SelectableViewModel SelectedItem
        {
            get { return selItem; }
            set 
            {
                if (selItem != value)
                {
                    selItem = value;
                    RaisePropertyChanged("SelectedItem");
                    RaisePropertyChanged("SelectedTemplate");
                }
            }
        } 

从上面的代码可以看出,除了宣布SelectedItem属性已更改外,代码还宣布了SelectedTemplate属性已更改。这使得框架重新读取暴露数据模板的属性。当应用新的数据模板时,项目将被正确显示。

测试应用程序

为了测试该解决方案,我构建了一个简单的标签UI。根据选定的标签,页面将显示一个不同的用户控件。由于WP7中没有TabControl,我将使用ListBoxContentControl的组合。页面UI的重要部分可以在下面的列表中看到。

 <ContentControl Content="{Binding SelectedItem}"
                            ContentTemplate="{Binding Path=SelectedTemplate}"
                                HorizontalContentAlignment="Stretch"
                                VerticalContentAlignment="Stretch"></ContentControl>
            <ListBox ItemsSource="{Binding Path=Items}" Grid.Row="1" Margin="5"
                     SelectedItem="{Binding Path=SelectedItem, Mode=TwoWay}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Path=DisplayName}" FontSize="30"
                                   FontWeight="Bold" Margin="5"/>
                    </DataTemplate>
                </ListBox.ItemTemplate>
                <ListBox.ItemsPanel>
                    <ItemsPanelTemplate>
                        <StackPanel Orientation="Horizontal"
                                    HorizontalAlignment="Center"></StackPanel>
                    </ItemsPanelTemplate>
                </ListBox.ItemsPanel>
            </ListBox> 

从上面的代码可以看出,我们在页面顶部有一个ContentControl。这个内容控件将用于根据列表中的选定项目显示数据。Content属性与视图模型中的SelectedItem属性进行数据绑定。每次从列表中选择一个新项目时,它都会被更改。ContentTemplate属性也进行了数据绑定,但这次是与SelectedTemplate视图模型属性进行绑定。这将设置相应的数据模板。

在内容控件下方是一个列表框。这个列表框将用于呈现选项卡。选项卡将具有水平对齐方式。选项卡名称将绑定到项目的DisplayName属性。同样,当列表框中的选定项目发生变化时,上面的内容控件显示的项目也会发生变化。

下图展示了测试应用程序的主屏幕

mainPageUI.png

视图模型和三个选项的视图之间的映射可以在下面的XAML中看到。

 <DataTemplate x:Key="FirstViewModel">
            <v:FirstView></v:FirstView>
        </DataTemplate>
        <DataTemplate x:Key="SecondViewModel">
            <v:SecondView></v:SecondView>
        </DataTemplate>
        <DataTemplate x:Key="ThirdViewModel">
            <v:ThirdView></v:ThirdView>
        </DataTemplate> 

从ViewModel中移除DataTemplate

为了从视图模型中移除数据模板,必须更改用于显示内容的content control。其中一个选项是派生另一个控件自content control并重写OnContentChanged()方法。每次控件的Content属性更改时都会调用此方法。依我看,这是放置数据模板选择代码的好地方。新的控件类可以在下面的代码中看到。

    public class DynamicContentControl:ContentControl
    {
        protected override void 
            OnContentChanged(object oldContent, object newContent)
        {
            base.OnContentChanged(oldContent, newContent);
            this.ContentTemplate = 
                DataTemplateSelector.GetTemplate(newContent as ViewModelBase);
        }        
    }  

从上面的代码可以看出,我首先调用基类实现,然后使用DataTemplateSelector类根据新内容的类型来设置ContentTemplate属性。

在XAML中使用这个新控件非常简单。代码可以在下面看到。

<loc:DynamicContentControl Content="{Binding SelectedItem}"
                                HorizontalContentAlignment="Stretch"
                                VerticalContentAlignment="Stretch" /> 

正如你所见,我不再绑定ContentTemplate属性。而且ViewModel中的SelectedTemplate属性也已被移除。

就是这样。希望你喜欢这篇文章。请随时发表评论和建议。

历史

  • 添加于2010年9月27日
© . All rights reserved.