代码改变世界

Expert C# 2008 Business Objects 第19章 WPF用户界面 NO.3

2009-07-13 17:21  sapajou  阅读(1001)  评论(0编辑  收藏  举报

2.6 登录窗体

MainForm中的LogInOut()方法调用了一个登录对话框来收集和验证用户的凭证,在收集了来自用户的凭证后,对话框窗体调用了PTPrincipal.Login()方法来进行验证。

图19-3 显示了登录窗体布局

clip_image002

图19-3 登录窗体布局

窗体定义了一个Result属性,该属性是个bool值,指示用户是否点击了“登录”按钮。

private bool _result;

public bool Result

{

get { return _result; }

set { _result = value; }

}

void LoginButton(object sender, EventArgs e)

{

_result = true;

this.Close();

}

void CancelButton(object sender, EventArgs e)

{

_result = false;

this.Close();

}

当用户点击“登录”或者“取消”按钮时,Result属性被设置为相应的值,对话框也被关闭。即使对话框被关闭了并且从屏幕上消失了,对话框对象仍然存在于内存中,它的属性对于应用程序中的代码也是有效的。LogInOut()方法依赖于Result属性,还有UserNameTextBox和PasswordTextBox控件的值来完成它的工作。

你已经从主窗体和登录窗体再到EditForm基类看到了应用程序基础结构是如何工作的,现在我要继续并展示如何实现一对实际的编辑窗体和对话框。我不会涵盖用户界面中的所有元素,因为它们遵循一些基本的主题,一旦你看到了一对,你也就理解了其余的。

2.7 RolesEdit窗体

RolesEdit用户控件允许一个经过授权的用户编辑当资源被指定给项目时所扮演的角色。这是要创建的用户界面中最简单的一个类型,因为Csla.Wpf命名空间中的业务对象和控件已经处理了大部分的工作。


图19-4显示了一个经过授权的用户编辑数据时的窗体。

clip_image004

图19-4 经过授权的用户使用RolesEdit窗体

构建这个窗体的大多数工作在XAML中,由于这是我讨论的第一个窗体,我将详细的进行解释。

2.7.1窗体声明

首先,这是窗体的声明:

<local:EditForm x:Class="PTWpf.RolesEdit"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

xmlns:local="clr-namespace:PTWpf"

xmlns:csla="clr-namespace:Csla.Wpf;assembly=Csla"

xmlns:ptracker=

"clr-namespace:ProjectTracker.Library.Admin;assembly=ProjectTracker.Library">

没有使用Window也没有使用UserControl基类,这里使用了local:EditForm,就是本章前面的EditForm类。注意local:前缀是在这个头部定义的:

xmlns:local="clr-namespace:PTWpf"

你可以认为这像一个using语句,将一个命名空间放到这个窗体的使用范围中。

那对所有xmlns语句来说都是正确的,因此,你能看到Csla.Wpf和ProjectTracker.Library.Admin命名空间也是有效的。

2.7.2窗体资源

XAML下面的小节定义了一些对整个窗体来说都有效的全局资源。

<local:EditForm.Resources>

<local:VisibilityConverter x:Key="VisibilityConverter" />

<csla:IdentityConverter x:Key="IdentityConverter" />

<csla:CslaDataProvider x:Key="RoleList"

ObjectType="{x:Type ptracker:Roles}"

FactoryMethod="GetRoles"

DataChanged="DataChanged"

ManageObjectLifetime="True">

</csla:CslaDataProvider>

<csla:ObjectStatus x:Key="RoleListStatus"

DataContext="{StaticResource RoleList}" />

</local:EditForm.Resources>

我在本章的前面讨论的VisibilityConverter类,这里你看到它是如何作为一个资源被定义的。IdentityConverter,CslaDataProvider和ObjectStatus类在第10章中讨论过。

IdentityConverter:解决了与WPF数据绑定有关的数据刷新的问题

CslaDataProvider:提供到Roles业务对象的数据绑定

ObjectStatus:将业务对象的状态属性提升为依赖属性,使得它们能被绑定到UI元素

2.7.2.1 CslaDataProvider

我想更多的关注一下CslaDataProvider,因为这是你第一次看到它起作用。像WPF的ObjectDataProvider一样,CslaDataProvider控件允许数据绑定访问一个作为数据源的对象。然而,CslaDataProvider远远超出了简单的ObjectDataProvider对象,它不仅能创建和获取业务对象,也能取消对对象的编辑和保存对象-所有这些都完全通过XAML来实现。让我们看一下前面的代码。

ObjectType属性指定了要获取的业务对象的类型,ptracker:Roles类型相当于来自ProjectTracker.Library.Admin命名空间的Roles类。

FactoryMethod属性指定了Roles类中静态工厂方法的名字,该方法将被调用来创建或者获取对象。在这个例子中,GetRoles()方法被调用。

DataChanged属性为DataChanged事件指定了事件处理器。要记得,这个窗体是从EditForm继承的,EditForm定义了一个DataChanged()方法来处理这个事件,因此,事件会被路由到那个处理器上。

最后,ManageObjectLifetime属性指定了由数据提供者来管理业务对象的生命周期,将值设置为True将告诉CslaDataProvider启用通过WPF命令来取消编辑、添加新项到集合、从集合中移除项的能力。

因为IsInitialLoadEnabled没有被指定,它的默认值是True,这意味着数据提供者在窗体初始加载的时候会调用工厂方法并加载业务对象。这也意味着当窗体被加载时会自动填入数据,这正是我们想要的。

由于IsAsynchronous没有被指定,它的默认值是False,这意味着数据提供者会在UI线程上调用工厂方法。将该值设为True将导致数据在一个后台线程中被加载,用户界面将在它们被返回到UI线程时显示这些值。

2.7.2.2 ObjectStatus

ObjectStatus控件被绑定到数据提供者,因此它的DataContext或是数据源是被数据提供者返回的业务对象。这意味着它将业务对象的诸如IsSavable和CanEditObject这样的属性暴露出来,所以你可以使用那些值在需要的时候控制UI元素的状态。这些属性与本章前面讨论的VisibilityConverter和ListTemplateConverter控件一起使用来控制当用户登录应用程序或是注销时UI如何反应。

2.7.3设置数据上下文

把CslaDataSource作为一个资源定义,它可以被用来设置窗体的部分或全部的DataContext,在本例中,它通过指定包含所有其他控件的Grid控件的DataContext属性为窗体中的所有内容控件设置了数据上下文。

<Grid Name="MainGrid"

DataContext="{Binding Source={StaticResource RoleList}}">

DataContext属性被设置为一个绑定表达式,因此源对象成了上面资源中定义的RoleList数据提供者。数据绑定知道当一个数据源是一个数据提供者控件时,它应该绑定到数据提供者的Data属性,在本例中,那是Roles业务对象。

2.7.4Grid资源

Grid也定义自己的资源,包括两个DataTemplate元素和一个ListTemplateConverter。一个DataTemplate定义了一个ListBox(或是相似控件)中的每行数据如何显示,一个ListBoxItem的默认模板仅仅显示数据对象的ToString()方法的值,这很少是我们期望的结果。定义一个DataTemplate允许你精确地控制每行数据如何被显示。

定义两个模板是为了处理授权。如果用户未被授权编辑值,只读模板将被使用。如果用户允许编辑值则读写模板被使用。这个技术意味着窗体能定义一个ListBox控件,但实际的显示可以基于用户的权限使用ListTemplateConverter很容易地被更改。

2.7.4.1 只读模板

每个DataTemplate定义了一个自包含的UI节,例如,只读模板看起来是这样的:

<DataTemplate x:Key="lbroTemplate">

<Grid>

<StackPanel Orientation="Horizontal">

<TextBlock Text="{Binding Path=Name}" Width="250" />

</StackPanel>

</Grid>

</DataTemplate>

这意味着ListBox控件中的每行数据将被显示在一个Grid控件中,Grid控件包含了一个StackPanel以固定的宽度显示一个TextBlock。

2.7.4.2 读写模板

读写模板有些复杂,包含了一对可编辑的TextBox控件和一个按钮,使得用户能从列表中移除项。

<DataTemplate x:Key="lbTemplate">

<Grid>

<StackPanel Orientation="Horizontal">

<TextBlock>Id:</TextBlock>

<TextBox x:Name="IdTextBox"

Text="{Binding Path=Id,

Converter={StaticResource IdentityConverter}}"

Width="100" />

<csla:PropertyStatus Source="{Binding}"

Property="Id" Grid.Column="1"

Target="{Binding ElementName=IdTextBox}" />

<TextBlock>Name:</TextBlock>

<TextBox x:Name="NameTextBox"

Text="{Binding Path=Name,

Converter={StaticResource IdentityConverter}}"

Width="250" />

<csla:PropertyStatus Source="{Binding}"

Property="Name" Grid.Column="1"

Target="{Binding ElementName=NameTextBox}" />

<Button

Command="ApplicationCommands.Delete"

CommandParameter="{Binding}"

CommandTarget=

"{Binding Source={StaticResource RoleList},

Path=CommandManager,

BindsDirectlyToSource=True}"

HorizontalAlignment="Left">Remove</Button>

</StackPanel>

</Grid>

</DataTemplate>

这个模板有很多东西,所以我会分解一下。

两个TextBox控件允许用户编辑Id和Name属性,每个TextBox都有一个关联的PropertyStatus控件,该控件为与那些业务对象属性关联的授权和验证规则提供了可视化提示。让我们看一下绑定到Name属性的TextBox。

TextBox

绑定到Name属性的TextBox有个显示声明的名称,因为PropertyStatus控件要通过这个名称引用这个控件。我在讲述PropertyStatus控件时会讨论这是如何工作的。这里是TextBox控件的声明。

<TextBox x:Name="NameTextBox"

Text="{Binding Path=Name,

Converter={StaticResource IdentityConverter}}"

Width="250" />

Text属性使用一个绑定表达式绑定到业务对象的Name属性。注意这个表达式使用了IdentityConverter,为了避免我在第10章讨论的字段刷新问题这是必需的,由业务对象到用户输入值的改动不总是反应在用户界面上。在绑定上使用一个值转换器可以避免这个问题,IdentityConverter应该被用到你不需要更具体的值转换器的所有的绑定上。

PropertyStatus

NameTextBox有一个关联的PropertyStatus控件。

<csla:PropertyStatus Source="{Binding}"

Property="Name" Grid.Column="1"

Target="{Binding ElementName=NameTextBox}" />


像在第10章中讨论的那样,PropertyStatus控件基于业务对象属性的授权和验证规则为用户提供可视化的提示。它依赖于用户对属性是否有读、写或是无的访问权限来启用或禁用目标控件(在本例中是NameTextBox)。如图19-5所示,显示了属性的某个打破的验证规则消息(为全部的三种严重性)。最后,如果有任何的异步验证规则正在执行,它会显示忙碌动画提醒用户注意与该属性相关的一些后台处理正在进行中。

clip_image006

图19-5 PropertyStatus控件显示验证错误

注意控件是如何通过设置Source属性为{Binding}来绑定到当前的DataContext的,业务对象属性的名称被通过“Property”属性来指定。

我通常将PropertyStatus控件与每个绑定到业务对象属性的细节控件关联起来以获得它提供的可视化提示。

使用ValidatesOnDataErrors

你应该知道WPF自身提供了非常有限的方式来利用业务对象属性的验证规则。你可以选择使用ValidatesOnDataErrors属性来声明TextBox控件的绑定表达式。

Text="{Binding Path=Name,

Converter={StaticResource IdentityConverter},

ValidatesOnDataErrors=True}"


ValidatesOnDataErrors属性指定了WPF应该检查业务对象的IDataErrorInfo接口,如果有任何的Error严重性规则被打破,它将更改TextBox控件的显示。默认情况下,它仅仅在控件周围添加一条细的红色边框,就像图19-6显示的那样,但是你可以按你的选择重写该样式。

clip_image008

图19-6 WPF使用ValidatesOnDataErrors显示一个验证错误

你可以与PropertyStatus控件一起使用这个特性或者是单独使用,这个特性和PropertyStatus控件是完全不相关的并且是兼容的。

使用命令的按钮控件

Remove按钮使用WPF命令与CslaDataProvider控件进行交互,这通过Command、CommandTarget和CommandParameter属性来控制。

<Button

Command="ApplicationCommands.Delete"

CommandParameter="{Binding}"

CommandTarget=

"{Binding Source={StaticResource RoleList},

Path=CommandManager,

BindsDirectlyToSource=True}"

HorizontalAlignment="Left">Remove</Button>

这些属性的工作如下:

Command:指定按钮单击时命令将被发送到的目标

CommandParameter:使用一个绑定表达式将一个当前子对象(子对象代表了ListBox中的这一行数据)的引用作为参数传递

CommandTarget:使用一个相对复杂的绑定表达式将命令引导到CslaDataProvider的CommandManager对象上

像在第10章讨论的那样,CslaDataProvider理解几个标准的命令,这包括Save、Undo、AddNew和Delete。当然,需要一个参数的只有Delete命令,因为CslaDataProvider需要知道哪个对象要从集合中移除。

也从第10章回忆一下,命令只能被视觉元素处理,而数据提供者不是一个视觉元素。为了支持命令,CslaDataProvider提供了一个CommandManager属性,该属性是一个视觉元素对象并且可以接受命令。绑定表达式将目标设置为这个CommandManager对象。

BindsDirectlyToSource属性必须被设置为True,因为RoleList资源是一个数据提供者。WPF数据绑定通常会明白资源是一个数据提供者并将所有的绑定路由到它的Data属性。然而,在本例中,绑定实际上需要路由到数据提供者自身而不它提供的业务对象。BindsDirectlyToSource属性使得数据绑定直接作用在资源上而不是Data属性。

你会在这个窗体的后面看到使用命令的Button控件的其他例子。

2.7.5窗体内容

窗体的内容由一个TextBlock标签、一个含有可编辑项的ListBox和一些按钮控件组成。

<StackPanel>

<TextBlock>Roles:</TextBlock>

<ListBox Name="RolesListBox"

ItemsSource="{Binding}"

ItemTemplate="{Binding Source={StaticResource RoleListStatus},

Path=CanEditObject,

Converter={StaticResource ListTemplateConverter}}" />

<StackPanel Orientation="Horizontal"

Visibility="{Binding Source={StaticResource RoleListStatus},

Path=CanEditObject,

Converter={StaticResource VisibilityConverter}}">

<Button

Command="ApplicationCommands.Save"

CommandTarget="{Binding Source={StaticResource RoleList},

Path=CommandManager, BindsDirectlyToSource=True}"

HorizontalAlignment="Left" IsDefault="True">Save</Button>

<Button

Command="ApplicationCommands.Undo"

CommandTarget="{Binding Source={StaticResource RoleList},

Path=CommandManager, BindsDirectlyToSource=True}"

HorizontalAlignment="Left" IsCancel="True">Cancel</Button>

<Button Name="AddItemButton"

Command="ApplicationCommands.New"

CommandTarget="{Binding Source={StaticResource RoleList},

Path=CommandManager, BindsDirectlyToSource=True}"

HorizontalAlignment="Left" IsCancel="True">Add role</Button>

</StackPanel>

</StackPanel>

用户界面的核心是ListBox控件,它显示了来自Roles业务对象的项,它是通过指定一个ItemTemplate做到这一点的,该ItemTemplate是一个定义了集合中的每一项如何在窗体上被显示的DataTemplate。

像你看到的那样,两个DataTemplate项在XAML前面的Grid.Resources元素中被定义了:一个是为了只读显示,一个是为了读写显示。ListTemplateConverter基于来自ObjectStatus控件的CanEditObject的值自动地在两个模板间进行切换,ObjectStatus控件在窗体的资源字典中被声明。ObjectStatus控件显露了正被窗体使用的业务对象的状态属性,因此,结果就是依赖于用户是否有权限编辑数据正确的模板(只读或者读写)将被显示给用户。

各种不同的Button控件也应该仅对于允许编辑数据的用户可见。因为这些控件使用WPF命令与CslaDataProvider控件交互,它们会基于业务对象是否允许用户保存、取消或者添加新项来自动地启用或者禁用。以保存按钮为例:CslaDataProvider控件使用业务对象的IsSavable属性来决定对象是否能被保存,因此按钮仅在对象的IsSavable属性为True时被启用。

不过,所有的按钮都包含在StackPanel控件中,它的Visibility属性被绑定到前面定义在窗体资源中的ObjectStatus控件的CanEditObject属性上。通过使用VisibilityConverter控件,CanEditObject的bool值被转换为一个Visibility值:Visible或是Collapsed。

<StackPanel Orientation="Horizontal"

Visibility="{Binding Source={StaticResource RoleListStatus},

Path=CanEditObject,

Converter={StaticResource VisibilityConverter}}">

结果就是如果用户被授权编辑业务对象那么他将会看到StackPanel的内容。

回到Button控件,我提到过他们使用WPF命令与CslaDataProvider控件交互,就像前面讨论的Remove按钮那样。

<Button

Command="ApplicationCommands.Save"

CommandTarget="{Binding Source={StaticResource RoleList},

Path=CommandManager, BindsDirectlyToSource=True}"

HorizontalAlignment="Left" IsDefault="True">Save</Button>

在这种情况下,保存命令被发送到数据提供者控件的CommandManager,使得业务对象(Roles集合)的任何变动都会被保存。

到此你已看到了定义窗体的所有XAML,几乎一个编辑窗体的所有行为都完全通过XAML被处理了,窗体后台只需要少量的代码。

2.7.6ApplyAuthorization方法

窗体后台需要的代码仅仅是一个ApplyAuthorization()方法的重写。MainForm将在用户登录应用程序或者注销的时候调用这个方法,所以这是基于新的用户标识所做的任何变动的地方。

当前.NET主体变动时,所有授权规则都要重新检查。XAML元素处理了所有的UI变动,便有些需要触发它们重新检查授权规则。最简单的方式就是把它们重新绑定到业务对象,这也是CslaDataProvider控件Rebind()方法的目的。

protected override void ApplyAuthorization()

{

var dp = (Csla.Wpf.CslaDataProvider)this.FindResource("RoleList");

dp.Rebind();

if (!Csla.Security.AuthorizationRules.CanEditObject(dp.ObjectType))

dp.Cancel();

}

这个方法中有另外一个功能:对CslaDataProvider控件的Cancel()方法的调用。如果新的用户没有被授权编辑业务对象,Cancel()方法将被调用。

这样做的原因是当用户注销时他可能正在编辑数据的过程中,业务对象和UI可能有很多变动的数据,但是现在用户已经注销了,那些变动不应该被保存也不应该被显示。调用Cancel()方法使得对象返回到一个未变动的状态同时数据绑定更新UI,以确保用户不会看到部分被编辑但实际上是无效的数据。

RolesEdit窗体到这就结束了,我涵盖了任何WPF数据实体窗体所需要的大部分重要概念。在本章剩余的部分,我会讨论应用程序中几个其他的窗体,集中在不同UI需求的微小的变化。