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

使用WPF MVVM、XAML模板和Entity Framework 6构建现代用户界面

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (15投票s)

2015年4月18日

CPOL

8分钟阅读

viewsIcon

89127

downloadIcon

6780

基于 WPF MVVM 和 Entity Framework 6 的业务应用程序。

引言

本文介绍如何从头开始构建一个具有Windows 8风格外观和感觉的WPF窗口。该解决方案基于MVVM(Model-View-ViewModel)架构。

背景

本文可能对具有C#、WPF和Entity Framework基础知识的中级开发人员和设计人员有所帮助。

Using the Code

在本文中,我们将通过三个步骤解释如何开发一个WPF应用程序,该应用程序从SQL SERVER数据库显示客户列表。

  1. Windows Presentation Foundation简介
  2. 使用Entity Framework 6进行数据库访问
  3. 解释MVVM架构

您可以下载所有源代码并将其改编到您的UI项目中,然后直接开始编写自定义业务代码,而无需在设计和用户界面上浪费时间。

I. Windows Presentation Foundation (WPF)

Windows Presentation Foundation是一个用于在Windows应用程序中渲染用户界面的图形子系统。WPF,以前称为“Avalon”,最初作为.NET Framework 3.0的一部分发布。WPF不依赖于较旧的GDI子系统,而是使用DirectX,并提供一致的编程模型来构建应用程序,从而将UI与业务逻辑分离。

在WPF之前,Microsoft开发人员使用了Windows Forms(Winforms),它基于GDI+和一些Windows内核库,这意味着我们无法完全控制Winforms。我们可以创建自己的控件,但无法修改任何现有控件的基础模板。

在WPF中,事情变得更容易,开发人员出于多种原因对UI组件拥有完全的控制权:

  • WPF是可扩展的,它使用一种称为XAML的XML风格编程语言。
  • 我们可以“彻底”改变用户控件的外观和感觉,而无需编写任何代码,这一切都基于资源和主题,并以XML风格语言编写。

让我们使用Visual Studio 2013和.NET 4.5.1编写一些XAML代码。请按照以下步骤操作:

  1. 打开 Visual Studio。
  2. 选择Visual C#并创建一个WPF应用程序。
  3. 将应用程序命名为“Code Project”。
  4. Visual Studio将生成一个默认的WPF窗口,名为MainWindow.xaml,其外观如下:

上图展示了Visual Studio生成的WPF窗口的默认模板,让我们仔细看看这个窗口背后的模板:

<?xml version="1.0" encoding="utf-16"?>
<ControlTemplate TargetType="Window" 
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
  <Border BorderThickness="{TemplateBinding Border.BorderThickness}" 
	BorderBrush="{TemplateBinding Border.BorderBrush}" 
	Background="{TemplateBinding Panel.Background}">
    <AdornerDecorator>
      <ContentPresenter Content="{TemplateBinding ContentControl.Content}" 
	ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}" 
	ContentStringFormat="{TemplateBinding ContentControl.ContentStringFormat}" />
    </AdornerDecorator>
  </Border>
</ControlTemplate>

默认模板包含4个主要类:

  • ControlTemplate:定义WPF窗口的模板。
  • Border:在其他元素周围绘制边框、背景或两者兼有。
  • AdornerDecorator:Adorner是FrameworkElement的一种特殊类型,用于向用户提供视觉提示。Adorner除其他用途外,还可以用于为元素添加功能手柄或提供控件的状态信息。
  • ContentPresenter:显示ContentControl的内容。

“模板”是本文的重点。我们希望通过使用XAML模板为原始WPF窗口添加一些功能,例如:

  • 删除边框
  • 添加右下角(窗口右下角)的调整大小抓手。
  • 在右上角边框添加新功能(例如:刷新、关于等)。
  • 窗口应响应不同设备(手机、平板电脑、PC等)。

让我们回到Visual Studio。

  1. 右键单击解决方案,然后添加一个名为“Themes”的新文件夹。
  2. 在此文件夹中添加一个新的资源字典,并将其命名为“Generic.xaml”。
  3. 将此代码粘贴到Generic.xaml中(请阅读其中的注释)。
         <!--Represents an object that describes the customizations to the non-client area of a window.-->
           <Setter Property="shell:WindowChrome.WindowChrome">
                <Setter.Value>
                    <shell:WindowChrome CornerRadius="0" GlassFrameThickness="1" />
                </Setter.Value>
            </Setter>
    
      <!--Customize the default template by Adding new controls (Buttons, Icons, etc.) 
          and modifying existent controls (Example : Border)-->
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type local:WindowBase}">
                        <Border BorderBrush="{DynamicResource WindowBorder}" BorderThickness="1">
                            <Border.Background>
                                <SolidColorBrush Color="{DynamicResource WindowBackgroundColor}" />
                            </Border.Background>
                            <Grid x:Name="LayoutRoot">
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="30" />
                                    <RowDefinition Height="*" />
                                    <RowDefinition Height="15" />
                                </Grid.RowDefinitions>
                                 <!--Display the Title of The Window-->
                                <TextBlock Text="{Binding Header, 
                                      RelativeSource={RelativeSource AncestorType=
                                      {x:Type local:WindowBase}}}"
                                           Grid.Row="0"
                                           Foreground="{DynamicResource ButtonTextDisabled}"
                                           HorizontalAlignment="Center"
                                           VerticalAlignment="Top"
                                           Margin="5"/>
                                <!--commandes (exemple : Minimise, Refrech, Avout,etc ...)-->              
                                <StackPanel Grid.Row="0"
                                            Orientation="Horizontal"
                                            HorizontalAlignment="Right"
                                            VerticalAlignment="Top"
                                            Margin="5"
                                            shell:WindowChrome.IsHitTestVisibleInChrome="True">
                                    <!--Adding new Command Called 'Preferences'-->
                                    <Button FontSize="12"
                                            FontWeight="Bold"
                                            Command="{Binding PreferencesWindowCommand, 
                                            RelativeSource={RelativeSource 
                                            AncestorType={x:Type local:WindowBase}}}"
                                            Style="{StaticResource SystemButton}">
                                        <Path Width="14" Height="14"
                                              Data="F1 M 38,23.5C 38.8643,23.5 39.7109,
                                              23.5756 40.5337,23.7206L 42.6275,18.5381L 48.1901,
                                              20.787L 46.0964,25.9692C 47.6473,27.0149 48.9851,
                                              28.3527 50.0308,29.9036L 55.213,27.8099L 57.4619,
                                              33.3725L 52.2794,35.4664C 52.4244,36.2891 52.5,
                                              37.1357 52.5,38C 52.5,38.8643 52.4244,39.7109 52.2794,
                                              40.5337L 57.4619,42.6275L 55.213,48.1901L 50.0308,
                                              46.0964C 49.0795,47.5073 47.8865,48.7418 46.5112,
                                              49.7405L 48.7844,54.8462L 43.3041,57.2891L 41.0307,
                                              52.1828C 40.0533,52.3906 39.0394,52.5 38,52.5C 37.1357,
                                              52.5 36.2891,52.4244 35.4664,52.2794L 33.3725,
                                              57.462L 27.8099,55.213L 29.9036,50.0309C 28.3527,
                                              48.9851 27.0149,47.6473 25.9691,46.0964L 20.787,
                                              48.1901L 18.538,42.6275L 23.7206,40.5336C 23.5756,
                                              39.7109 23.5,38.8643 23.5,38C 23.5,37.1357 23.5756,
                                              36.2891 23.7206,35.4664L 18.538,33.3725L 20.787,
                                              27.8099L 25.9691,29.9036C 26.9205,28.4927 28.1135,
                                              27.2582 29.4889,26.2594L 27.2157,21.1537L 32.6959,
                                              18.7109L 34.9694,23.8172C 35.9468,23.6094 36.9606,
                                              23.5 38,23.5 Z M 38,28C 32.4771,28 28,32.4772 28,
                                              38C 28,43.5229 32.4771,48 38,48C 43.5228,48 48,
                                              43.5229 48,38C 48,32.4772 43.5228,28 38,28 Z"                                         
                                              Stretch="Fill"
                                              Fill="{Binding Foreground, 
                                              RelativeSource={RelativeSource 
                                              Mode=FindAncestor, AncestorType=Button}}"
                                              Stroke="{Binding Foreground, 
                                              RelativeSource={RelativeSource 
                                              Mode=FindAncestor, AncestorType=Button}}"
                                              StrokeThickness="0.1" />
                                    </Button>
                                    <!--Command About-->
                                    <Button Content="?"
                                            Command="{Binding AboutWindowCommand, 
                                            RelativeSource={RelativeSource 
                                            AncestorType={x:Type local:WindowBase}}}"
                                            FontSize="13"
                                            FontWeight="Bold"
                                            Style="{StaticResource SystemButton}"/>
                                    <!--Commande Minimize-->
                                    <Button Command="{Binding MinimizeWindowCommand, 
                                    RelativeSource={RelativeSource 
                                    AncestorType={x:Type local:WindowBase}}}"
                                            ToolTip="{Binding MinimizeWindowToolTip, 
                                            RelativeSource={RelativeSource 
                                            AncestorType={x:Type local:WindowBase}}}"
                                            Style="{StaticResource SystemButton}">
                                        <Button.Content>
                                            <Grid Width="13"
                                                  Height="12"
                                                 RenderTransform="1,0,0,1,0,1">
                                                <Path Data="M0,6 L8,6 Z"
                                                      Width="8"
                                                      Height="7"
                                                      VerticalAlignment="Center"
                                                      HorizontalAlignment="Center"
                                                      Stroke="{Binding Foreground, 
                                                      RelativeSource={RelativeSource 
                                                      Mode=FindAncestor, AncestorType=Button}}"
                                                      StrokeThickness="2"  />
                                           </Grid>
                                       </Button.Content>
                                    </Button>         
                                    <!--Command Restore-->
                                    <Button x:Name="Restore"
                                                Command="{Binding RestoreWindowCommand, 
                                                RelativeSource={RelativeSource 
                                                AncestorType={x:Type local:WindowBase}}}"
                                                ToolTip="{Binding RestoreWindowToolTip, 
                                                RelativeSource={RelativeSource 
                                                AncestorType={x:Type local:WindowBase}}}"
                                                Style="{StaticResource 
                                                SystemButton}" Visibility="Collapsed" >
                                            <Button.Content>
                                                <Grid Width="13"
                                                      Height="12"
                                                      UseLayoutRounding="True"
                                                      RenderTransform="1,0,0,1,.5,.5">
                                                    <Path Data="M2,0 L8,0 L8,
                                                    6 M0,3 L6,3 M0,2 L6,2 L6,8 L0,8 Z"
                                                          Width="8"
                                                          Height="8"
                                                          VerticalAlignment="Center"
                                                          HorizontalAlignment="Center"
                                                          Stroke="{Binding Foreground, 
                                                          RelativeSource={RelativeSource 
                                                          Mode=FindAncestor, AncestorType=Button}}"
                                                          StrokeThickness="1"  />
    
                                                </Grid>
                                            </Button.Content>
                                       </Button>
                                    <!--Command Maximize-->
                                    <Button x:Name="Maximize"
                                                Command="{Binding MaximizeWindowCommand, 
                                                RelativeSource={RelativeSource 
                                                AncestorType={x:Type local:WindowBase}}}"
                                                ToolTip="{Binding MaximizeWindowToolTip, 
                                                RelativeSource={RelativeSource 
                                                AncestorType={x:Type local:WindowBase}}}"
                                                Style="{StaticResource SystemButton}" >
                                        <Button.Content>
                                            <Grid Width="13"
                                                      Height="12">
                                               <Path Data="M0,1 L9,1 L9,8 L0,8 Z"
                                                         Width="9"
                                                          Height="8"
                                                          VerticalAlignment="Center"
                                                          HorizontalAlignment="Center"
                                                          Stroke="{Binding Foreground, 
                                                          RelativeSource={RelativeSource 
                                                          Mode=FindAncestor, AncestorType=Button}}"
                                                          StrokeThickness="2"  />
                                            </Grid>
                                        </Button.Content>
                                    </Button>
                                    <!--Command Close-->
                                    <Button Command="{Binding CloseWindowCommand, 
                                    RelativeSource={RelativeSource 
                                    AncestorType={x:Type local:WindowBase}}}" 
                                            ToolTip="{Binding CloseWindowToolTip, 
                                            RelativeSource={RelativeSource 
                                            AncestorType={x:Type local:WindowBase}}}"
                                            Style="{StaticResource SystemButton}" >
                                        <Button.Content>
                                           <Grid Width="13"
                                                  Height="12"
                                                 RenderTransform="1,0,0,1,0,1">
                                                <Path Data="M0,0 L8,7 M8,0 L0,7 Z"
                                                      Width="8" Height="7"
                                                      VerticalAlignment="Center"
                                                      HorizontalAlignment="Center"
                                                      Stroke="{Binding Foreground, 
                                                      RelativeSource={RelativeSource 
                                                      Mode=FindAncestor, AncestorType=Button}}"
                                                      StrokeThickness="1.5"  />
                                            </Grid>
                                        </Button.Content>
                                    </Button>
                                </StackPanel>                     
                                <!--Content Presenter-->
                                <ContentPresenter Grid.Row="1" Margin="40" />                          
                                <!-- Project Title-->
                                <Border Grid.RowSpan="3"
                                        Background="{DynamicResource Accent}"
                                        RenderTransform="1,0,0,1,0,32"
                                        MinWidth="14" MinHeight="53"
                                        HorizontalAlignment="Left"
                                        VerticalAlignment="Top">
                                    <TextBlock Text="{TemplateBinding Title}"
                                              Foreground="White"
                                               Margin="0,8"
                                               VerticalAlignment="Center">
                                        <TextBlock.LayoutTransform>
                                            <RotateTransform Angle="-90" />
                                        </TextBlock.LayoutTransform>
                                    </TextBlock>
                                </Border>
    
                                <!-- Resize Grip, we can add here for example a status Bar, etc .-->
                                <Grid Grid.Row="2">
                                    <Path x:Name="ResizeGrip"
                                          Visibility="Collapsed"
                                          Width="12"
                                          Height="12"
                                          Margin="1"
                                          HorizontalAlignment="Right"
                                          Stroke="{DynamicResource WindowText}"
                                          StrokeThickness="1"
                                          Stretch="None"
                                          Data="F1 M1,10 L3,10 M5,10 L7,10 M9,10 L11,10 M2,
                                          9 L2,11 M6,9 L6,11 M10,9 L10,11 M5,6 L7,6 M9,6 L11,
                                          6 M6,5 L6,7 M10,5 L10,7 M9,2 L11,2 M10,1 L10,3" />
                                </Grid>
                            </Grid>
                        </Border>               
                    </ControlTemplate>
                </Setter.Value>
            </Setter>

结果如下

让我们做一个比较。

  1. 第一个窗口展示了具有附加功能(关于、首选项等)的自定义模板,它是一个chrome窗口,无边框,可通过右下角的调整大小抓手进行调整。
  2. 第二个是我们由Visual Studio生成的默认模板(未经定制),它包含基本功能(最小化、最大化、关闭)。

关于Button类还有最后两点说明。在最后一个代码片段中,添加新按钮时,我使用了:

  1. Command属性:这是一个依赖项属性,它表示一个输入命令,将在下一段(解释MVVM架构)中详细介绍。
  2. SystemButton:这是一个附加到Button的样式,它来自一个名为FirstFloor.ModernUI的额外库。您可以通过Nuget包管理器将其添加到您的解决方案中。
<Button Content="?" Command="{Binding AboutWindowCommand, 
RelativeSource={RelativeSource AncestorType={x:Type local:WindowBase}}}" 
FontSize="13" FontWeight="Bold" Style="{StaticResource SystemButton}"/>

在理解了自定义模板和XAML之后,让我们编写一些C#代码来解释MVVM模式。

II. 解释MVVM架构

Model-View-ViewModel(MVVM)模式有助于您清晰地分离应用程序的业务逻辑与其用户界面(UI)。保持应用程序逻辑和UI之间的清晰分离有助于解决许多开发和设计问题,并可以使您的应用程序更易于测试、维护和演进。

它还可以极大地提高代码重用机会,并使开发人员和UI设计人员在开发应用程序的各自部分时更容易协作。MVVM建立在这三个组件之上:

  • Model:它指的是领域模型,它代表真实的状态内容(面向对象的方法),或者指的是代表该内容的数据访问层(以数据为中心的方法)。
  • ViewModel:ViewModel是View的抽象,它公开public属性和命令。
  • View:与MVC和MVP模式一样,View是用户界面(UI)。

  1. 使用Entity Framework 6实现Model

Entity Framework(EF)是一个开源]的ADO.NET对象关系映射(ORM)框架,是.NET Framework的一部分。您可以从http://entityframework.codeplex.com/.下载源代码。下图说明了Entity Framework访问数据的架构:

对象服务是一个组件,它使开发人员能够使用实体类型的实例的CLR对象查询数据库(insertselectupdateremove)。对象服务支持Linq(语言集成查询)。

此外,ADO.NET始终用于使用ADO.NET数据提供程序查询数据库,该数据提供程序从实体客户端数据提供程序接收查询,并将对象集合返回给调用者。Entity Framework支持两种场景(Database First和Code First)。

在本解决方案中,我们将使用Database First方法。请遵循以下步骤:

  • 在解决方案中创建一个名为“Models”的文件夹。
  • 右键单击解决方案资源管理器 => 添加 => 新建项 => 数据 => ADO.NET实体数据模型。
  • Visual Studio助手将建议许多选项(Database First、Code First等)。在本例中,我们将选择Database First。
  • 助手将连接到我们的数据库并提取数据(表、存储过程等)。

单击OK,助手将生成访问数据库所需的实体。

Entity Framework完成了所有必要的映射(表到类)。现在,我们只能使用对象开始查询数据库。我们的数据库名为“Demo”,因此EF 6.0将生成一个名为“DemoEntities”的类,该类继承自System.Data.Entity.DbContext

使用Entity Framework,所有内容都由对象表示。它创建了一个抽象层,使应用程序独立于数据库。如果我们想使用Oracle或MySQL数据库,编程模型将保持不变,我们只需安装目标数据库的Entity Framework提供程序。

我们的Model已完成,让我们来实现第二个组件“ViewModel”,它将查询Model。

2. 实现ViewModel

ViewModel与Model进行交互,并充当Model和View之间的中介。我们将创建一个名为“CommandBase.cs”的基类,用于所有View-Model类。此基类包含基本的属性和命令,然后我们将编写第二个类,名为“ClientViewModel.cs”。ClientViewModel.cs负责查询客户数据并将其显示在Client View中。

CommandBase.cs是所有View-Model类的基类。

  • 它是一个泛型类,可以与数据库中的任何对象(Client、Command、Foo等)一起使用。
  • 它实现了“INotifyPropertyChanged”,因此能够通知View。
  • 它包含一个“ICommand”类型的属性,该属性表示从View发送到ViewModel的命令(例如:Get命令,它将执行Get方法)。Get方法将在派生类中被重写(请参阅ClientViewModel.cs)。
  • 像“CanGet”这样的布尔属性在我们想要以干净的方式激活/停用某些控件时非常有用。
  • 一些属性和方法被标记为Virtual关键字,因此我们可以在每个派生类中重新定义它们。

    在定义了所有View-Model类的基类之后,让我们编写一个View-Model类的示例(ClientViewModel.cs)。

    ClientViewModel 类实现了CommandBase 类,并传递了实体“Client”。我们还可以注意到派生类如何重写某些属性和方法(如GetCanGet),以便它们可以针对Client 实体工作。搞定!:)

3. 实现View

View包含一个按钮和一个数据网格。该按钮必须通过绑定(Command="{Binding GetCommand}")连接到一个命令。当用户单击该按钮时,GetCommand 将在ViewModel中执行。此命令将从Client 表中检索数据,并将它们复制回我们的Collection of Client

当我们的“Collection”填充了数据后,Data Grid将向最终用户显示这些数据,因为它的Item 源与“Collection”相关联。

    <Window.Resources>
        <local:ClientViewModel x:Key="clientViewModel"/>
    </Window.Resources>

    <Grid  DataContext="{StaticResource ResourceKey=clientViewModel}">
        <Grid.RowDefinitions>
                <RowDefinition  Height="Auto" />
                <RowDefinition  Height="9*" />
            </Grid.RowDefinitions>
        <Grid Grid.Row="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Button  x:Name="BtnDisplayClient" Grid.Column="0"
                     Content="Display"
                     HorizontalAlignment="Center" Margin="2"
                     Command="{Binding GetCommand}"/>
        </Grid>
        <DataGrid x:Name="clientDataGrid" Grid.Row="1"
                      Margin="0,10,0,0"
                      ItemsSource="{Binding Collection}"
                      AutoGenerateColumns="False">
            <!--ItemsSource="{Binding Clients}"-->
            <DataGrid.Columns>
                <DataGridTextColumn Header="Nom"        
                Binding="{Binding Path=Nom}"/>
                <DataGridTextColumn Header="Profession" 
                Binding="{Binding Path=Profession}"/>
            </DataGrid.Columns>
            </DataGrid>
   </Grid>

最终结果是:此WPF窗口中的所有组件都可以通过XAML模板进行自定义。

摘要

我想写一篇文章,将WPF MVVM、XAML模板和Entity Framework结合到一个解决方案中。无论您是Windows Forms的拥护者还是WPF的拥护者,我都希望您能欣赏我的努力。感谢您阅读我的博文,尝试下载源代码,如果您愿意,请随时留下您的问题、评论和感谢。

© . All rights reserved.