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

并发可排序集合和 AOP

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2012年11月20日

CPOL

4分钟阅读

viewsIcon

18862

downloadIcon

129

如何在动态 WPF 应用程序中使用面向切面的编程工具包

引言

今天我读了这篇文章,并且秉承我“懒惰是发明的母亲”的座右铭,我立刻考虑是否能通过利用现有的 .Net 并发集合和我最喜欢的 AOP 工具集来实现相同的结果。别误会,这篇文章的方法没有问题,但我试图像其他懒人一样,尽量避免重复造轮子。

背景

在我看来,面向切面编程(AOP)是一项我并非完全理解,但在日常使用中非常享受的技术。就像我的汽车、互联网骨干网或 Entity Framework。我简化 AOP 的首选工具是 PostSharp,来自 SharpCrafters 的优秀团队。此外,使用 ConcurrentBag 和 ConcurrentDictionary,对于大多数应用程序来说是线程安全的,并且性能很高。因此,唯一剩下的挑战是如何在最小的手写代码的情况下,实现从不同线程到 GUI 线程的后台更新。

在这两种情况下,PostSharp 工具集都非常有用。在本应用中,我们将使用其中的两个。

让我们从创建一个新的 WPF 应用程序开始……然后打开 NuGet 程序包管理器控制台并安装以下程序包

Install-Package PostSharp.ToolKit.Domain

Install-Package PostSharp.ToolKit.Threading

这将把 AOP 库添加到默认项目中。如果你是 GUI 导向的,可以通过右键单击解决方案节点并选择“管理解决方案的 NuGet 程序包…”来完成同样的操作。

我也喜欢在日常工作中使用 Reactive Extensions,所以让我们也添加 Rx-Main 程序包。这个不是必需的,它只是让创建可观察事件更有趣。

Install-Package Rx-Main

我们还将使用 Managed Extensibility Framework,所以继续添加

System.ComponentModel.Composition

现在是激动人心的部分了……

使用代码

我们将从创建我们的 Customer 类开始

using System;
using PostSharp.Toolkit.Domain;

namespace WpfWithAopAndPtl
{
    [NotifyPropertyChanged]
    internal class Customer
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }

        [NotifyPropertyChangedSafe]
        public string FullName
        {
            get { return String.Join(", ", LastName, FirstName); }
        }
    }
}

\[NotifyPropertyChanged] 属性由 PostSharp Domain Toolkit 提供。简而言之,它会自动处理以下场景

  • 自动和字段备份的属性;
  • 复杂属性直接或间接依赖于同一类中的字段和其他属性;
  • 复杂属性依赖于同一类中的方法(只要它们不依赖于其他类中的方法);
  • 复杂属性依赖于提供更改通知的子对象的属性(在简单属性链的情况下自动,在任意复杂代码的情况下基于显式声明的依赖项)。

换句话说,它消除了大量您本来需要手动编写的代码。但是,由于其(截至 2012 年 11 月)的一些限制,它将无法处理

  • 依赖于其他对象上的方法的属性 getter(除了上述情况)
  • 比属性 getter 调用链更复杂的依赖于外部对象属性的情况
  • 依赖于虚拟方法的属性
  • 属性 getter 中委托的直接或间接使用
  • 依赖于静态字段
  • 依赖于集合中项的属性

为了支持这些场景,它仍然允许您实现 INotifyPropertyChanged 接口(如果您还有另一个很棒的工具,JetBrains 的 ReSharper,它将生成额外的代码使这个过程更简单)。

为了适应可观察的线程安全集合,我选择使用 ConcurrentBag<T> 并通过 IQueryable<T> 接口公开它。这样,数据会尽快从模型返回,将排序留给 UI 中的其他类(这不是 SOLID 原则吗……)。

这是模型接口

using System.Linq;
namespace WpfWithAopAndPtl
{
    internal interface IMainViewModel
    {
        string Title { get; }
        IQueryable<Customer> Customers { get; }
        void Add(Customer c);
    }
}

以及模型的实现

using System.Collections.Concurrent;
using System.ComponentModel;
using System.ComponentModel.Composition;
using System.Linq;
using PostSharp.Toolkit.Domain;
using WpfWithAopAndPtl.Annotations;
namespace WpfWithAopAndPtl
{
    [Export(typeof (IMainViewModel))]
    [NotifyPropertyChanged]
    internal class MainViewModel : IMainViewModel, INotifyPropertyChanged
    {
        private readonly ConcurrentBag<Customer> _customers;

        public MainViewModel()
        {
            _customers = new ConcurrentBag<Customer>();
        }

        public string Title
        {
            get { return "AOP Sample"; }
        }

        [NotifyPropertyChangedIgnore]
        public IQueryable<Customer> Customers
        {
            get { return _customers.AsQueryable(); }
        }

        public void Add(Customer c)
        {
            _customers.Add(c);
            OnPropertyChanged("Customers");
        }

        public event PropertyChangedEventHandler PropertyChanged;

        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

所以,我再次应用 NotifyPropertyChanged 属性,然后声明公共 Customers 属性要被排除在 AOP 处理之外,并在 Add() 方法中手动引发 OnPropertyChanged 事件。我还用 Export 属性装饰了模型,以便以后可以使用 MEF 来发现它。

让我们看一下主窗口的代码隐藏

using System;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using PostSharp.Toolkit.Threading;

namespace WpfWithAopAndPtl
{
    /// <summary>
    ///     Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private readonly Random _rand = new Random(DateTime.Now.Millisecond);
        [Import(typeof (IMainViewModel))] private IMainViewModel _model;
        private bool _shuttingdown;

        public MainWindow()
        {
            InitializeComponent();
            SatisfyImports();
            DataContext = _model;
            Loaded += MainWindow_Loaded;
            Closing += (sender, args) => { _shuttingdown = true; };
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            Task.Factory.StartNew(() =>
                {
                    IObservable<long> timer = Observable.Interval(TimeSpan.FromSeconds(1));
                    using (timer.Subscribe((s) => AddCustomer()))
                    {
                        while (!_shuttingdown)
                        {
                            Thread.Sleep(50);
                        }
                    }
                });
        }

        [DispatchedMethod]
        private void AddCustomer()
        {
            var customer = new Customer
                {
                    Id = _model.Customers.Count(),
                    FirstName = Names.FirstNames[_rand.Next(Names.FirstNames.Length)],
                    LastName = Names.LastNames[_rand.Next(Names.LastNames.Length)]
                };
            _model.Add(customer);
        }

        private void SatisfyImports()
        {
            var catalog = new AssemblyCatalog(typeof (MainWindow).Assembly);
            var container = new CompositionContainer(catalog);
            container.SatisfyImportsOnce(this);
        }
    }
}

首先,我声明我将请求 MEF 提供一个实现 IMainViewModel 接口的类。导入在 SatisfyImports 方法执行期间得到满足,在那里我只是请求当前程序集用作所有部件的目录。现在,在一个真实的生产场景中,您可以使用 CompositeCatalog 和 DirectoryCatalog,并将当前程序集中的类与在某些 DLL 中找到的类混合。接下来,我将窗口的上下文设置为新发现的模型,并继续创建一个基于 Rx 的计时器,该计时器每秒触发一个事件,导致创建并添加到集合中一个随机的 Customer,使用模型的公共 Add() 方法。AOP 的 DispatchMethod 属性只是允许 UI 的更新从后台线程执行。否则,如果方法不是在 GUI 线程上执行,WPF 将生成一个异常。

为了公开客户的动态集合,我选择使用一个简单的 ListBox,并带有 DataTemplate。

 <Window x:Class="WpfWithAopAndPtl.MainWindow"
        xmlns="<a href="http://schemas.microsoft.com/winfx/2006/xaml/presentation">http://schemas.microsoft.com/winfx/2006/xaml/presentation</a>"
        xmlns:x="<a href="http://schemas.microsoft.com/winfx/2006/xaml">http://schemas.microsoft.com/winfx/2006/xaml</a>" xmlns:wpfWithAopAndPtl="clr-namespace:WpfWithAopAndPtl"
        xmlns:componentModel="clr-namespace:System.ComponentModel;assembly=WindowsBase"
        Title="{Binding Title}" Height="350" Width="525">

遵循面向对象设计的 SOLID 原则,我将排序客户集合的责任留给数据消费者。市面上有很多控件,比如 Telerik 或 Infragistics,它们还允许额外的功能,例如过滤或数据虚拟化。所以 IMHO,将集合公开为 IQueryable 是有意义的。

这就是在 WPF 中公开多线程可观察集合所需的一切了。

享受附带的代码并提交您的评论。


© . All rights reserved.