并发可排序集合和 AOP





5.00/5 (1投票)
如何在动态 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 中公开多线程可观察集合所需的一切了。
享受附带的代码并提交您的评论。