zoukankan      html  css  js  c++  java
  • WPF 跟踪命令和撤销命令(复原)

        WPF 命令模型缺少一个特性是复原命令。尽管提供了一个 ApplicationCommands.Undo 命令,但是该命令通常被用于编辑控件(如 TextBox 控件),以维护它们自己的 Undo 历史。如果希望支持应用程序范围内的 Undo 特性,就需要在内部跟踪以前的状态,并且触发 Undo 命令时还原该状态。

        遗憾的是,扩展 WPF 命令系统并不容易。相对来说没有几个入口点可以使用连接自定义逻辑。为了创建通用、可重用的 Undo 特性,需要创建一组全新的“能够撤销命令的”命令类,以及一个特定类型的命令绑定。我们需要设计自己的用于跟踪和复原命令的系统,使用 CommandManager 类保存命令历史。下图显示了本文的例子。在该例子中,窗口包含两个文本框和一个列表框,可以自由地在这两个文本框中输入内容,而列表框则一直跟踪在这两个文本框中发生的所有命令。可以通过单击 ‘复原’ 按钮还原最后一个命令。

        该例使用一个名为 CommandHistoryItem 的类来存储信息状态,例如:刚刚删除的字符。

        每个 CommandHistoryItem 对象跟踪以下几部分信息:

    • 命令名称
    • 执行命令的元素。
    • 在目标元素中被改变了的属性
    • 保存目标元素受影响以前状态的对象

        这一设计非常巧妙,它为一个元素存储状态。如果存储整个窗口状态的快照,会显著增加内存的使用量。然而,如果具有大量数据(如文本框有几十行文本),Undo 操作的负担就很大了。解决方法是限制在历史中存储项的数量,或者使用更加智能(也更加复杂)的方法只存储被改变的数据信息,而不是存储所有数据。

        CommandHistoryItem 类还提供了一个通用的 Undo() 方法。该方法使用反射为修改过的属性应用以前的值,用于恢复 TextBox 控件中的文本,但是对于更复杂的应用程序,需要用到 CommandHistoryItem 类的层次结构,每个类都可以使用不同方式还原不同类型的操作。

        下面是 CommandHistoryItem 类的完整代码:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using System.Text;
    using System.Windows;
    
    namespace _1028_UndoCommand
    {
        public class CommandHistoryItem
        {
            /// <summary>
            /// 命令名称
            /// </summary>
            public string CommandName { get; set; }
            
            /// <summary>
            /// 执行命令的元素
            /// </summary>
            public UIElement ElementActedOn { get; set; }
            
            /// <summary>
            /// 在目标元素中被改变了的属性
            /// </summary>
            public string PropertyAcedOn { get; set; }
            
            /// <summary>
            /// 保存目标元素受影响以前状态的对象
            /// </summary>
            public object PreviousState { get; set; }
    
            public CommandHistoryItem(string commandName)
                : this(commandName, null, string.Empty, null)
            { }
    
            /// <summary>
            /// 初始化CommandHistoryItem对象
            /// </summary>
            /// <param name="commandName">命令名称</param>
            /// <param name="elementActedOn">执行命令的元素</param>
            /// <param name="propertyAcedOn">在目标元素中被改变了的属性</param>
            /// <param name="previousState">保存目标元素受影响以前状态的对象</param>
            public CommandHistoryItem(string commandName, UIElement elementActedOn, string propertyAcedOn, object previousState)
            {
                this.CommandName = commandName;
                this.ElementActedOn = elementActedOn;
                this.PropertyAcedOn = propertyAcedOn;
                this.PreviousState = previousState;
            }
    
            /// <summary>
            /// 获取是否能够撤销操作
            /// </summary>
            public bool CanUndo
            {
                get
                {
                    return (ElementActedOn != null && PropertyAcedOn != string.Empty);
                }
            }
    
            /// <summary>
            /// 复原
            /// </summary>
            public void Undo()
            {
                //使用反射技术将值还原回去
                Type elementType = ElementActedOn.GetType();
                PropertyInfo property = elementType.GetProperty(PropertyAcedOn);
                property.SetValue(ElementActedOn, PreviousState, null);
            }
        }
    }

        还需要的下一个要素是执行应用程序范围内的 Undo 操作的命令。 ApplicationCommands.Undo 命令是不合适的,因为为了不同的目的,它已经被用于单独的文本框控件(复原最后的编辑变化)。所以我们需要创建一个新的命令,如下:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows.Input;
    
    namespace _1028_UndoCommand
    {
        /// <summary>
        /// 复原命令
        /// </summary>
        public class UndoCommands
        {
            static RoutedUICommand applicationUndo;
    
            /// <summary>
            /// 复原命令
            /// </summary>
            public static RoutedUICommand ApplicationUndo
            {
                get { return UndoCommands.applicationUndo; }
            }
    
            static UndoCommands()
            {
                applicationUndo = new RoutedUICommand("撤销操作", "Application Undo", typeof(UndoCommands));
            }
        }
    }

        到目前为止,除了执行 Undo 操作的反射代码有点儿意思之外,其他代码没有什么值得注意的地方。更困难的部分是将该命令历史集成到WPF命令模型中,所以这里我们需要使用到 CommandManager 类中的一个事件。 该类提供了几个静态事件,这些事件包括 CanExecute、PreviewCanExecute、Executed 以及 PreviewExecuted 。 在本示例中, Executed 和 PreviewExecuted 事件是最有趣的,因为无论在何时,当执行任何一个命令都会引发它们。然而, Executed 事件是在命令执行完之后被触发的,这时已经来不及在命令历史中保存被影响的控件状态了。所以我们需要响应 PreviewExecuted 事件,该事件在命令执行之前一刻被触发。

        下面是关联 PreviewExecued 事件的 处理程序,并且当关闭窗口时接触关联。

            public MainWindow()
            {
                InitializeComponent();
                this.AddHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(CommandExecuted));
            }
    
            private void Window_Unloaded_1(object sender, RoutedEventArgs e)
            {
                this.RemoveHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(CommandExecuted));
            }

        当触发事件时,我们需要将历史记录到项中(ListBox),于是有了下面的代码:

            void CommandExecuted(object sender, ExecutedRoutedEventArgs e)
            {
                //忽略按钮源
                if (e.Source is ICommandSource)
                    return;
    
                //忽略自编写的ApplicationUndo命令
                if (e.Command == UndoCommands.ApplicationUndo)
                    return;
    
                TextBox txt = e.Source as TextBox;
                if (txt == null)
                    return;
    
                //记录修改之前的数据到集合中
                RoutedCommand cmd = (RoutedCommand)e.Command;
                CommandHistoryItem historyItem = new CommandHistoryItem(cmd.Name, txt, "Text", txt.Text);
    
                ListBoxItem item = new ListBoxItem();
                item.Content = historyItem;
                lstHistory.Items.Add(historyItem);
            }

        该示例在 ListBox 控件中存储了所有 CommandHistoryItem 对象。 ListBox 控件的 DisplayMember 属性被设置为 CommandName ,所以它会显示每个项目的 CommandHistoryImte.CommandName 属性。以上代码只为由文本框引发的命令提供 Undo 特性。然而,处理窗口中的任何文本框通常足够了。为了支持其他控件和属性,需要对代码进行扩展。

        最后一个细节是执行应用程序范围内 Undo 操作的代码。使用 CanExecute 事件处理程序,可以确保只有当在 Undo 历史中至少有一项时,才能执行Undo操作:

            //评估复原命令是否可用
            private void CommandBinding_CanExecute_1(object sender, CanExecuteRoutedEventArgs e)
            {
                if (lstHistory == null || lstHistory.Items.Count == 0)
                    e.CanExecute = false;
                else
                    e.CanExecute = true;
            }

        为了恢复最近的修改,只需要调用 CommandHistoryItem 的 Undo() 方法,然后从 ListBox 项中删除该项即可:

            //执行复原命令
            private void CommandBinding_Executed_1(object sender, ExecutedRoutedEventArgs e)
            {
                CommandHistoryItem historyImte = (CommandHistoryItem)lstHistory.Items[lstHistory.Items.Count - 1];
    
                if (historyImte.CanUndo)
                    historyImte.Undo();
    
                lstHistory.Items.Remove(historyImte);
            }

        至此,该示例就结束了。尽管该示例演示了相关概念,并提供了一个简单的应用程序,但是要在实际的应用程序中使用这一方法,还需要进行许多改进。例如,需要花费大量的时间改进 CommandMamager.PreviewExecuted 事件的处理程序,以忽略哪些明显不需要跟踪的命令(当前,如使用键盘选择文本的事件以及单击空格键引发的命令等)。

    源码下载:http://files.cnblogs.com/andrew-blog/1028_UndoCommand.rar

    开发工具:VS2012

    参考:http://www.wxzzz.com/WPF/WPF_GenZongMingLing_CheXiaoMingLing

  • 相关阅读:
    bzoj3295: [Cqoi2011]动态逆序对
    bzoj3262: 陌上花开
    bzoj1176: [Balkan2007]Mokia
    bzoj1935: [Shoi2007]Tree 园丁的烦恼
    [APIO / CTSC2007]数据备份 --- 贪心
    [APIO2007]风铃 --- 贪心
    [NOI2015]寿司晚宴 --- 状压DP
    [NOI2007]货币兑换 --- DP + 斜率优化(CDQ分治)
    [NOI2009]诗人小G --- DP + 决策单调性
    [HNOI2008]玩具装箱TOY --- DP + 斜率优化 / 决策单调性
  • 原文地址:https://www.cnblogs.com/andrew-blog/p/WPF_UndoCommand.html
Copyright © 2011-2022 走看看