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

oops - A Cross-Platform, General Purpose Undo/Redo Framework

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2018年10月8日

CPOL

6分钟阅读

viewsIcon

8366

downloadIcon

6

oops 框架的设计、实现和使用

引言

许多面向用户的应用程序通常希望为用户驱动的操作提供某种撤销/重做功能。通用的撤销/重做框架应该跨平台、可配置且结构松散,以适应不可预见的用法需求(由UI驱动?由服务驱动?并发访问者?等)。

背景

创建通用撤销/重做框架的难点在于解决(至少)两个核心问题:

1. 能够将一系列用户操作定义为可撤销
2. 使用户所做的每一件事都可撤销
  • 例如,使每次输入的字符都可撤销,可能会导致糟糕的用户体验,但嘿,这是个开始。
  • 理想的框架允许代码聚合更改
  • oops 允许进行配置或介于两者之间的操作。这完全取决于你创建和使用的 Accumulator 的范围。

总体目标:以有序的方式观察系统中所有对象的更改,针对 N 个未知对象

例如,任何给定的用户操作可能会创建和/或修改 50 个对象,通过在更改时明确告知每个对象如何执行以及如何撤销,将所有更改汇总为一个用户撤销操作,这至少是繁琐的,在任何实际用例中都是不可能的。oops 通过观察 ViewModel(派生自 TrackableViewModel)中的任何和所有属性更改以及任何集合更改(TrackableCollection - list, stack, queue - 或 TrackableDictionary),将所有这些管理起来,并将它们聚合为全局或局部的单个撤销操作。

为什么区分全局和局部更改?很高兴你问了。

假设你的应用程序有一个主窗口/窗体/页面之类的东西。应用程序中的任何操作都将被观察并合并到撤销操作中,通过一个由菜单驱动的撤销/重做系统(就像我们习惯的后退和前进箭头一样)。

现在,假设你有一个弹出对话框,其中进行了一些可撤销的操作,然后又弹出了另一个进行了一些可撤销操作的对话框。根据项目经理的意愿,这两个对话框的所有操作都应该合并为一个可撤销的用户操作,或者应该有两个。oops 通过创建单例/全局作用域或局部作用域的选项,使得这两种情况(以及我认为能想到的任何情况)都可用,这两种作用域都可以添加到全局撤销堆栈中。

支持任何 ViewModel 或 Collection 更改的撤销/重做组件

  • TrackableCollection - ObservableCollection 的增强版

    • 它会为你处理切换到 UI 线程的上下文(如果设置为在 UI 线程上触发)
    • 无缝跟踪所有操作并使其可撤销 - 随时可以打开/关闭
    • 按照 UI 所需的顺序、在正确的线程上、与调用线程同步地触发 CollectionChanged 和 PropertyChanged 事件 - 随时可以关闭
    • 可以很好地处理并发(从多个线程对其进行大量操作,它会一切处理妥当)
    • 还可以作为 Stack<> 或 Queue<> 工作(因为有时你需要将它们绑定到 UI)
    • 高性能(这个词还在用吗?),只要 UI 允许
  • TrackableDictionary - Dictionary 的超级增强版

    • 按照 UI 所需的顺序、在正确的线程上、与调用线程同步地触发 CollectionChanged 和 PropertyChanged 事件
    • 可以很好地处理并发(从多个线程对其进行大量操作,它会一切处理妥当)
    • 它会为你处理切换到 UI 线程的上下文(如果设置为在 UI 线程上触发)
    • 无缝跟踪所有操作并使其可撤销 - 随时可以打开/关闭
    • 高性能,只要 UI 允许并且绑定到 UI
  • ConcurrentList

    • 一个高性能的 List<>,可以很好地处理并发
    • 拥有许多便捷的方法,如 RemoveAndGetIndex、ReplaceAll 等,这些方法本身就是线程安全的
    • 也可以作为 Stack<> 或 Queue<> 工作
  • Accumulator

    • 记录所有操作以便以后撤销
    • 可以作为单例用于应用程序范围的操作,也可以作为实例用于局部范围的操作(例如在对话框中)
  • AccumulatorManager

    • 管理 Accumulator 的 Undo 和 Redo 堆栈
    • 自动记录对任何撤销操作的重做操作,反之亦然
  • TrackableViewModel

    • 自动跟踪其在引用的 Accumulator(单例或本地 Accumulator)中的所有属性更改
    • 通过使用自定义的 Get<>/Set<> 方法来跟踪任何属性更改
  • TrackableScope

    • 实用类,用于轻松创建 Accumulator 并在 Dispose 时将其推送到 AccumulatorManager
    • 在其
    using(new TrackableScope("testing"))

    块内的所有更改都将聚合为一次用户撤销操作

使用代码

  1. 创建一个继承自 TrackableViewModel 的 ViewModel
  2. 为其添加一些属性,如下所示
public bool ILikeCheese 
{
   get => Get<bool>();
   set 
   {
      if(Set(value))
        MessageBox.Show("Why did you change your mind?");
   } = true;
}
  1. 对于你需要的任何集合,请使用 TrackableCollection(最常用)
  2. 当你想要跟踪更改时,创建你的作用域,如下所示
using (new TrackableScope("Undo me now!"))
{
   ... do some stuff ...
}

或者,只需将 Globals.ScopeEachChange 设置为 true。

就是这样!AccumulatorManager 为你提供了可在 UI 中使用的绑定属性/命令,用于撤销你已跟踪的任何内容。

关注点

该框架经过了 4 年多的设计、提炼和改进,我认为是时候发布它了,以便为任何想为其应用程序添加此功能的人提供帮助。

与任何处理高性能并发活动的庞大任务的框架一样,有可能创建线程锁,所以请继续阅读以了解如何避免这种情况。 :)

如何使用 oops 框架创建线程锁?

不幸的是,如果你不清楚 TrackableCollection/Dictionary 由于大多数 UI(如 WPF)的特性而如何进行并发锁定(集合更改 CollectionChanged 事件需要同步发生),那么创建线程锁就非常简单。在这种情况下,锁必须设置在 UI 线程上。我真诚地希望情况并非如此,并且我确实付出了很多努力来找到一种方法来避免这种情况,但 WPF 在经历了大量的痛苦之后,扼杀了所有此类希望。我在 .NET Core Xaml 方面的经验并未让我相信它在这方面比 WPF 更具灵活性。

考虑到任何 TrackableCollection/Dictionary 的锁都会发生在 UI 线程上,创建线程锁定情况变得轻而易举。假设你有这个糟糕的扩展方法(即使这是一个糟糕的方法,你也能明白我的意思)

public static void AddAndRemoveOneSecondLater<TType>(this TrackableCollection<TType> coll, TType item)
{
    lock(coll.SyncRoot)
    {
       coll.Add(item);
       Thread.Sleep(60000);
       coll.Remove(item);
    }
}

如果你有这个方法,而且你不是新手,你不会因为 Thread.Sleep() 调用而从 UI 线程调用它,因为那样会很不专业。确实很不专业!

所以,如果从某个后台线程(比如线程池)调用它,你将在后台线程上锁定,然后 Add() 也将在 UI 线程上锁定同一个对象。由于 Add() 是同步的,你现在就造成了一个线程锁,因为 UI 线程在等待你的后台线程释放锁,但你的后台线程在等待 UI 线程上的 Add() 完成。

历史

oops 框架可在 github 上找到,并可在 nuget 上下载。  尽情享用!

© . All rights reserved.