代码改变世界

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

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

1 WPF中的自定义身份验证

WPF不是线程安全的,这意味着所有的UI工作将在单一的UI线程内完成。然而,WPF却在幕后使用后台线程。.Net中的安全基础结构是被当前的主体对象所驱动的,该主体对象与特定的线程相关。这意味着你能碰到这样的情况,你的代码运行在没有正确的主体对象的线程下。

这个问题仅适用于你正在使用自定义身份验证的情况,如果你正在使用Windows身份验证,那么所有的线程都将为登录到工作站的用户使用WindowsPrincipal对象,这就没有问题了。

有许多使用自定义身份验证和自定义主体对象的可能的场景,但是都有同样的问题:一个或多个线程以不正确的主体对象而告终。这是个问题,因为CSLA.NET的授权和数据门户子系统依赖.NET的主体对象而工作,如果不能依赖一个有效的主体,授权将很难实现,数据门户也不能可靠地在应用服务器上模拟客户端用户的身份。

在这方面WPF的行为是有意的:微软试图在一个线程在其他线程上偶然地变换主体对象的场景下保护应用程序,这种保护的不幸的副作用是构建一个使用自定义身份验证的WPF应用几乎是不可能的。

核心的问题是WPF不允许一个线程更改其他线程的当前主体对象,甚至是当你试图这样去做的时候,WPF基础结构也会重置主体对象为一个默认值。

微软推荐的方案是在应用程序域上设置默认的主体值:

AppDomain.CurrentDomain.SetPrincipal(principal);

一旦这行代码执行,该点之前所有创建的线程都将使用新的默认主体。然而,那有个很严重的局限,因为那意味着你需要在所有后台线程被创建前执行这行代码,包括那些线程池中的线程。这意味着你的登录过程不能使用任何的异步行为(不能有异步的数据门户或服务的调用),并且那在你应用程序的生命周期中一定要发生的非常早以确保没有其他的异步操作发生过。

这行代码在应用程序生命周期中只能被调用一次。所以,如果你的应用程序允许用户注销登录并在晚些时候再次登录(不关闭应用程序),你根本不能使用这个技术。

CSLA.NET ApplicationContext对象有一部分针对这一问题的工作,解决了所有CSLA.NET代码的问题,以及使用ApplicationContext.User对象访问当前主体的代码。那是个局部的解决方案,因为CSLA.NET不能确保在每个线程上的实际的CurrentPrincipal对象总是正确的,因此,所有的CSLA.NET代码会工作的很好,但一些.NET的特性-最明显的是代码访问安全(CAS)-会无法正常运行。

我讨论这个问题是因为WPF开发者普遍遇到过这个问题,虽然CSLA.NET在很大程度上解决了这一问题,你也应该理解该问题是存在的并且是由于WPF阻止一个线程变更被其他线程使用的主体对象的工作所导致的。

只要你使用Csla.ApplicationContext.User访问主体对象,这也是CSLA.NET自身的做法,你就可以在应用程序生命周期内变更主体对象,并且对所有线程来说都有一个一致的有效值。

2 界面设计

用户界面应用程序可以在ProjectTracker解决方案中找到,项目被命名为PTWpf。PTWpf界面包含了一个单一的主窗体,左侧有一系列导航链接,右面是内容区域。主窗体动态加载用户控件到内容区域,将它们显示给用户。主窗体如图19-1所示:clip_image002

图19-1 主窗体外观
注意左侧的导航项处理项目、资源、角色和身份验证。当用户单击一个链接时,一个用户控件被动态加载到窗体的主区域(内容区)。图19-2显示了当用户编辑一个项目时应用程序的外观。

clip_image004

图19-2 编辑一个项目

当然,一些对话框窗体被用来收集来自用户的输入,但是应用程序功能的绝大部分以宿主在主窗体中的用户控件的使用为中心。表19-1列出了组成用户界面的窗体和控件。

表19-1 PTWpf中的窗体和用户控件

窗体/控件

类型

描述

MainForm

Window

应用程序主窗体

EditForm

UserControl

创建用户控件的自定义基类

ListTemplateConverter

IValueConverter

将一个Boolean值转换为DataTemplate值

Login

Window

收集用户凭证的登录对话框

RolesEdit

EditForm

允许用户编辑角色列表

ProjectSelect

Window

提示用户从项目列表中选择的对话框

ProjectList

EditForm

显示项目列表

ProjectEdit

EditForm

允许用户查看、增加、编辑项目

ResourceSelect

Window

提示用户从资源列表中选择的对话框

ResourceList

EditForm

显示资源列表

ResourceEdit

EditForm

允许用户查看、增加、编辑资源

VisbilityConverter

IValueConverter

将一个Boolean值转换为Visibility值

本章采用的用户控件方法给了你很大的灵活性,你可以将用户控件宿主在一个MDI用户界面的子窗体中,像本章展示的那样,或者你也可以将用户控件宿主在一个多窗格用户界面的窗格中。总之,通过创建你的窗体作为用户控件,你获得了将他们用在多种不同类型用户界面设计中的灵活性。

2.1 用户控件框架

动态加载用户控件不难,代码需要遵循这个基本的过程:

1、 创建EditForm控件

2、 从视图中移除所有存在的EditForm控件

3、 添加一个新的EditForm到Children集合中

4、 为新的EditForm注册TitleChanged事件

尽管我本章的意图不是为你创建一个全套的WPF用户界面框架,但你必须做些基础的工作来提供一个令人满意的用户体验。

这里列出的步骤需要在每个用户控件和作为宿主的MainForm之间进行一些交互,为了标准化这个过程,我创建了一个EditForm类,这是UserControl类的一个子类。这个类提供了MainForm期望它承载的每个用户控件所具有的一些标准行为。

2.1.1EditForm类

EditForm类的主要目的是标准化每个控件如何在MainForm中被宿主,不过,它也提供了一些对数据绑定窗体有用的公共的行为。表19-2列出了EditForm提供的行为。

表19-2 EditForm实现的行为

行为

描述

Principal changed

启用一个模式,MainForm可以通知活动的EditForm当前的.NET主体对象已经变更

Provides a Titlte property

通过添加一个Title属性扩展用户控件,该属性可以通过XAML或代码设置;包含了一个事件,使得MainForm知道Title已经变动从而更新主窗体的标题

Error handling

为所有的数据提供者提供一个标准的方式来显示在数据访问期间发生的异常

我将分别讨论每个行为。

2.1.1.1 Principal changed

当前.NET主体对象变更时,意味着用户登录应用程序或者注销登录(登出),因此,所有的授权规则必须被重新应用于当前显示的EditForm。一个用户登录或者注销的确切的影响在于个别的EditForm,所以基类仅仅通过调用一个虚方法来通知子类。

void EditForm_Loaded(object sender, System.Windows.RoutedEventArgs e)

{

    ApplyAuthorization();

}

void IRefresh.Refresh()

{

    ApplyAuthorization();

}

protected virtual void ApplyAuthorization()

{

}

ApplyAuthorization()方法在EditForm第一次加载和MainForm调用Refresh()方法时都会被调用,因为当前主体对象已经变动。

每一个个别的子类都可以重写ApplyAuthorization()方法以重新应用基于新的.NET主体对象的授权规则。当我讨论一个实际的EditForm实现时会展示这是如何工作的。

2.1.1.2 Title Property

在这个应用程序中,我想让每个EditForm都能够变更顶层窗体的标题。那个标题是由MainForm控制的,因些需要一些机制让每个EditForm控件能请求MainForm变更标题。

在WPF中,可进行数据绑定的控件属性被实现为依赖属性,依赖属性与普通的属性很类似,但它受WPF(WF)基础结构支配,因此依赖属性能通过XAML绑定到值。下面是Title属性的定义:

public event EventHandler TitleChanged;

public static readonly DependencyProperty TitleProperty =DependencyProperty.Register(“Title”,typeof(string),typeof(EditForm),null);

public string Title

{

    get { return (string)GetValue(TitleProperty);}

    set

    {

        SetValue(TitleProperty,value);

        if (TitleChanged != null)

            TitleChanged(this,EventArgs.Empty);

    }

}

TitleChanged事件是个标准的事件,MainForm处理这个事件使得当值变动时被通知。属性声明自己是一个依赖属性,因此值能通过XAML设置,例如,这里是一个EditForm实例如何被声明的:

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

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

Title="Edit a Project">

Title属性可以在XAML中设置或是通过code-behind来设置,两者任何一个,TitleChanged事件都会被触发,MainForm就可能更新主窗体的标题了。

2.1.1.3 Error Handling

当为了数据访问使用一个WPF的数据提供者时,异常处理有点棘手。原因是数据访问是被控件触发的,而不是你的代码。因此,你没有办法将操作包装在try…catch语句块中来处理所有的异常。

标准的技术是处理数据提供者控件的DataChanged事件,看看数据提供者控件的Error属性是否为null,如果不为null则发生了异常。

对这个简单的样例应用程序,我选择了仅仅给用户显示异常的详细信息。EditForm基类提供了一个事件处理器来处理所有这样的异常,而不是在每个EditForm中重复那段代码。

protected virtual void DataChanged(object sender,EventArgs e)

{

    var dp = sender as System.Windows.Data.DataSourceProvider;

    if (dp.Error != null)

        MessageBox.Show(dp.Error.ToString(),"Data error",MessageBoxButton.OK,MessageBoxImage.Exclamation);

}

每个EditForm负责将数据提供者的DataChanged事件和这个事件处理器挂接,这可以通过XAML或是code-behind来完成。当我讨论个别的EditForm实现时将展示这是如何完成的。

2.1.2MainForm Window

公共代码的其他区域在MainForm类中。MainForm作为整个应用程序的外壳框架,宿主并协调所有的其他窗口和EditForm。在本节中,我只讨论与EditForm交互所需的代码,将导航和身份验证留到本章稍后讨论。

MainForm的内容区是一个DockPanel控件。

<DockPanel Grid.Column="1" Name="contentArea" Margin="5,15,20,15" />

这个DockPanel占据了窗体右边的所有可用空间,保留了一点边距以使内容位于圆角中。通过将控件命名为contentArea,窗体的后台代码就能够操作这个控件。

通过在contentArea中显示正确的内容来从一个EditForm移动到另一个,MainForm实现了一个ShowControl()方法来完成导航:

public static void ShowControl(UserControl control)

{

    _mainForm.ShowUserControl(control);

}

private void ShowUserControl(UserControl control)

{

    UnhookTitleEvent(_currentControl);

    contentArea.Children.Clear();

    if (control != null)

        contentArea.Children.Add(control);

    _currentControl = control;

    HookTitleEvent(_currentControl);

}

ShowControl()方法很容易被项目中的代码访问到,因为它是一个公共静态方法。不过,实际的MainForm实例必须显示用户控件,因此静态方法委托给了一个实际MainForm实例的实例方法,_mainForm字段引用了一个MainForm的实例,它的值在MainForm的构造函数中被设置。

ShowUserControl()方法从任何存在的控件上取消了TitleChanged事件的注册。

private void UnhookTitleEvent(UserControl control)

{

    EditForm form = control as EditForm;

    if (form != null)

        form.TitleChanged -= new EventHandler(SetTitle);

}

ShowUserControl()方法然后清空了contentArea的Children集合,确保DockPanel是空的,然后添加新的用户控件到Children集合中,实际上显示了新的内容。

最后,注册新控件的TitleChanged事件。

private void HookTitleEvent(UserControl control)

{

    SetTitle(control, EventArgs.Empty);

    EditForm form = control as EditForm;

    if (form != null)

        form.TitleChanged += new EventHandler(SetTitle);

}

这个方法也基于新用户控件的当前标题立即设置了主窗口的标题,这是通过调用SetTitle()方法完成的。

private void SetTitle(object sender, EventArgs e)

{

    EditForm form = sender as EditForm;

    if (form != null && !string.IsNullOrEmpty(form.Title))

        _mainForm.Title = string.Format("Project Tracker - {0}", ((EditForm)sender).Title);

    else

        _mainForm.Title = string.Format("Project Tracker");

}

注意MainForm是如何保持对主窗口标题的控制的,使用当前用户控件的Title属性来更改整体的值。这有助于保持应用程序的可维护性,因为窗口标题被控制在一个中心位置。

到这你应该对MainForm如何宿主用户控件以及EditForm基类如何容易地为此目的创建标准的用户控件有了一定的理解。

2.2 值转换

WPF允许你直接通过XAML做大量的工作,最小化每个窗口或用户控件的后台代码。你可以使用的将代码转换为XAML的一个强有力的技术就是实现一个值转换器控件。这类控件将一个类型的值,例如bool,转换为一个类型或者值。

要创建一个值转换器,你需要创建一个实现了IValueConverter接口的类,该接口定义在System.Windows命名空间。这个接口需要你实现表19-3列出的方法。这些方法在需要时被WPF数据绑定所调用。

表19-3 IValueConverter定义的方法

方法

描述

Convert

将一个数据源值转换为一个UI值

ConvertBack

将一个UI值转换为一个数据源值

一个值转换器的一个一般用处是将来自于一个业务对象的DateTime属性转换为一个显示给用户的正确地格式化过的字符串。那符合大多数人关于数据绑定如何工作的设想。

然而,在WPF中,数据绑定更为有用也更广泛。你可以使用数据绑定连接控件和其他控件或是对象,潜力是令人惊讶的。例如,在PTWpf中,我使用数据绑定和值转换器使得用户界面对自动响应授权规则。基于用户是否被授权创建或编辑业务对象,我使用数据绑定来启用或禁用用户界面控件,完全地隐藏一些控件,甚至变更ListBox控件中数据显示的方式。

做到这一点需要一对值转换器将一个bool值转换为一个DataTemplate(改变ListBox的显示方式)或是转换为一个Visibility值(显示或隐藏UI的一部分)。

我会先来讨论VisibilityConverter,因为它是两个中比较简单的。

2.2.1VisibilityConverter

VisibilityConverter类实现了IValueConverter接口,被设计来将一个bool值转换为一个Visibility值。它被用在将UI的部件绑定到一个像CanEditObject这样的授权属性时,这样一来,当用户能编辑对象时,Visibility值返回Visible,如果用户不允许编辑对象,Visibility值返回Collapsed。

public class VisibilityConverter : System.Windows.Data.IValueConverter

{

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)

    {

        if ((bool)value)

            return System.Windows.Visibility.Visible;

        else

            return System.Windows.Visibility.Collapsed;

    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)

    {

        return false;

    }

}

在这个例子中只有Convert()方法被完整地实现了,因为转换器被用来从一个ObjectStatus控件到一个UI控件进行绑定,UI控件的改变不用绑定回到ObjectStatus。

在Convert()方法中,value参数被转换为bool值,该值被用来决定哪个Visibility值作为结果返回。

当我讨论RolesEdit窗体时你将看到这是如何被使用的。

2.2.2ListTemplateConverter

ListTemplateConverter有点复杂,因为它实现IValueConverter接口的同时定义了一对属性。

public class ListTemplateConverter : System.Windows.Data.IValueConverter

{

    public System.Windows.DataTemplate TrueTemplate { get; set; }

    public System.Windows.DataTemplate FalseTemplate { get; set; }

    #region IValueConverter Members

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)

    {

        if ((bool)value)

            return TrueTemplate;

        else

            return FalseTemplate;

    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)

    {

        return value;

    }

    #endregion

}

再一次只有Convert()方法被完整地实现了,因为转换器被用来从一个ObjectStatus控件到一个UI控件的转换,如果UI控件有变动,不用绑定回ObjectStatus。

TrueTemplate和FalseTemplate属性用来定义基于输入的bool值转换器将返回的DataTemplate值,在你的XAML中,这个转换器作为一个资源被定义,像这个样子:

<local:ListTemplateConverter x:Key="ListTemplateConverter" TrueTemplate="{StaticResource editableTemplate}" FalseTemplate="{StaticResource readonlyTemplate}" />

这两个属性被设置为在同一个XAML文件中定义的DataTemplate项。当我在本章后面讨论RolesEdit窗体的时候你会看到关于这个转换器的一个例子。

像VisibilityConverter一样,这个转换器的逻辑仅仅是使用value参数来决定哪个结果被返回。WPF中的值转换器概念是非常强大的,你可以使用它将每页中的大量后台代码转换为可重用的控件,这些控件可以用于从XAML中定义每个页面。