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

在 WPF 中实现响应式 UI 元素

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (9投票s)

2012 年 7 月 12 日

CPOL

4分钟阅读

viewsIcon

35534

downloadIcon

525

如何使用MVVM设计模式在WPF中实现响应式UI元素。

引言

本文延续了来自文章“在WPF中实现UI元素授权”的思想。虽然它们没有直接关系,但理解该文章中提出的概念,如果您想改进之前的解决方案,可能会很有用。响应式UI元素背后的思想非常简单。以网络连接为例,分布式的WPF和Silverlight应用程序,高度依赖网络连接(内网或互联网)来访问服务器上的资源。该应用程序可能会在没有网络连接(离线)时限制用户执行某些操作,但仍然允许那些不依赖于网络连接和/或服务器资源的那些操作。在编程环境中,应用程序希望在网络连接可用时启用UI元素,并在网络连接丢失时禁用它们。

在传统的实现中,应用程序会设置一个事件处理程序,并监听来自某个组件(自包含的网络检测逻辑)的在线/离线事件,并在连接状态改变时对所有受影响的UI元素执行预期操作(启用/禁用)。每当需要在此过程中添加或删除参与的UI元素时,都会变得繁琐且容易出错。或者更糟的是,开发人员必须在不同的窗体中复制这段代码。在响应式UI元素中,只需将这段代码封装在一个自包含的行为(响应式UI行为)中,并将其附加/绑定到参与UI元素的属性上。简单且可重用!

通过响应式UI行为,应用程序可以轻松实现以下功能:

  1. 在网络连接可用或不可用时,动态启用或禁用UI元素。
  2. 在网络连接可用或不可用时,动态显示或隐藏UI元素。
  3. 根据权限动态强制执行UI元素访问控制。(请参阅在WPF中实现UI元素授权
  4. 在表单验证通过或失败时,分别动态启用或禁用“提交”按钮。
  5. 根据昼/夜模式改变UI窗体的背景颜色。
  6. 您的想象力将在此继续...

背景

MarkupExtension 是响应式UI行为的构建块,它使我们能够嵌入业务逻辑并将其声明式地关联或绑定到XAML中的UI元素。创建响应式UI行为的过程很简单,只需按照下面概述的步骤进行:

  1. 创建一个类并继承自抽象类MarkupExtension
  2. 重写ProvideValue函数。
  3. ProvideValue函数的IServiceProvider参数中获取IProvideValueTarget服务提供程序的引用。
  4. 通过IProvideValueTarget接口维护对TargetObjectDependencyObject)和TargetPropertyDependencyProperty)的引用。
  5. 设置事件处理程序并订阅通知事件。
  6. 在事件处理程序的回调时,使用新评估的值更新引用的TargetProperty(DependencyProperty)。

在本文中,我在演示项目中创建了两个响应式UI行为。它们的实现如下所示:

  • ConnectionAwareToEnabled - 可以绑定到具有IsEnabled属性的UI元素。
  • ConnectionAwareToVisibility - 可以绑定到具有Visibility属性的UI元素。

ConnectionAwareToEnabled

[MarkupExtensionReturnType(typeof(bool))]
public class ConnectionAwareToEnabled : MarkupExtension
{
    private DependencyObject    TargetObject    { get; set; }
    private DependencyProperty  TargetProperty  { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var provider = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
        if (provider != null)
        {
            3) Gets a reference of IProvideValueTarget provider from the IServiceProvider parameter
            if (provider.TargetObject is DependencyObject && provider.TargetProperty is DependencyProperty)
            {
                //  4) Maintains a reference to the TargetObject and TargetProperty
                TargetObject    = (DependencyObject)    provider.TargetObject;
                TargetProperty  = (DependencyProperty)  provider.TargetProperty;
            }
            else
            {
                throw new InvalidOperationException("The binding target is not a " + 
                   "DependencyObject or its property is not a DependencyProperty.");
            }
        }
        //  5) Subscribe to the event notification
        ConnectionManager.Instance.ConnectionStatusChanged += OnConnectionStateChanged;

        //  The initial value when the UI element is displayed
        return ConnectionManager.Instance.State == ConnectionState.Online;
    }

    private void OnConnectionStateChanged(object sender, ConnectionStateChangedEventArgs e)
    {
        //  6) Update the DependencyProperty value in event handler
        TargetObject.Dispatcher.BeginInvoke(new Action(() => TargetObject.SetValue(
           TargetProperty, e.CurrentState == ConnectionState.Online)));
    }
}

ConnectionAwareToVisibility

[MarkupExtensionReturnType(typeof(Visibility))]
public class ConnectionAwareToVisibility : MarkupExtension
{
    private DependencyObject    TargetObject    { get; set; }
    private DependencyProperty  TargetProperty  { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var provider = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
        if (provider != null)
        {
            3) Gets a reference of IProvideValueTarget provider from the IServiceProvider parameter
            if (provider.TargetObject is DependencyObject && 
                        provider.TargetProperty is DependencyProperty)
            {
                //  4) Maintains a reference to the TargetObject and TargetProperty
                TargetObject    = (DependencyObject)    provider.TargetObject;
                TargetProperty  = (DependencyProperty)  provider.TargetProperty;
            }
            else
            {
                throw new InvalidOperationException("The binding target is not " + 
                   "a DependencyObject or its property is not a DependencyProperty.");
            }
        }
        //  5) Subscribe to the event notification
        ConnectionManager.Instance.ConnectionStatusChanged += OnConnectionStateChanged;

        //  The initial value when the UI element is displayed
        return (ConnectionManager.Instance.State == 
                  ConnectionState.Online) ? Visibility.Visible : Visibility.Collapsed;
    }

    private void OnConnectionStateChanged(object sender, ConnectionStateChangedEventArgs e)
    {
        //  6) Update the DependencyProperty value in event handler
        TargetObject.Dispatcher.BeginInvoke(new Action(() => TargetObject.SetValue(TargetProperty,
           (ConnectionManager.Instance.State == ConnectionState.Online) ? 
                         Visibility.Visible : Visibility.Collapsed)));
    }
}

敏锐的读者可能已经注意到,上述实现利用了引用的TargetObjectDispatcher来更新UI元素的属性,这避免了来自后台线程的回调的跨线程封送问题。

使用代码

在XAML中使用响应式UI行为与普通数据绑定类似,只是它节省了您输入Binding关键字。下面的代码片段展示了如何在XAML标记中应用它们。“r”是响应式UI行为的命名空间引用。

<Button IsEnabled="{r:ConnectionAwareToEnabled}">Browse</Button>

<Button Visibility="{r:ConnectionAwareToVisibility}">Browse</Button>

本文包含的演示项目展示了Button等UI元素在网络连接可用时会被启用或可见,在不可用时则会被禁用或隐藏。要进行测试,您可以在控制面板中禁用或启用网络设备(控制面板 -> 网络和Internet -> 网络连接)。请耐心等待至少5秒钟!

关注点

最后但同样重要的是,有经验的读者或开发人员可能已经意识到上面的代码列表确实存在内存泄漏问题。内存泄漏是由于ConnectionManager对事件侦听器(响应式UI行为)持有强引用,从而阻止垃圾回收器对其进行收集。请放心,演示项目中包含的完整代码具有适当的实现,它遵循弱事件模式,并使用WeakEventManagerIWeakEventListener接口来避免内存泄漏问题。请务必查看演示项目中包含的以下代码文件:

  • ConnectionAwareToEnabledExtension.cs
  • ConnectionAwareToVisibilityExtension.cs
  • ConnectionStateEventManager.cs

最后,但同样重要的是,您可以从本文顶部的链接下载示例。

历史

  • 2012年7月12日:初始版本。
© . All rights reserved.