WPF之修改变色,回滚功能的实现

来源:互联网 发布:dior homme男装知乎 编辑:程序博客网 时间:2024/06/07 09:57

时隔多年,居然会写一篇关于.NET的文章, 而且还是关于WPF的。真是意外!

这里我就不给出相应的demo了;如果有缘人恰好有这个需求并且自己无法成功,就麻烦到时候再私信我吧。

1. 起因

最近在给公司写内部CS程序时,碰到这样一个需求——进行TextBox内容或者ComboBox的内容编辑时,我如何知道该输入/选择框的值被我修改过?另外一个与此紧密相关的就是编辑器里常见的修改回滚功能。因为关系紧密,所以本博客将两者一起进行讲解,节省篇幅,也节省各位看官的时间。

其实这个需求在我做专职WPF开发时就遇到过,当时的解决方案是使用.NET的TypeDescriptionProvider注解来为VM提供额外的属性。当时因为水平有限,觉得它比较复杂;加之这么多年了也完全忘记了其中的细节(根据这些年的感悟,之前的水平低可以接受,这笔记没做好就不能忍了。我那逝去的年华,唉!)。所以这次干脆换了种实现方式。得益于从事Java之后的这么几年里源码的学习,这次的实现过程中感觉顺手了不少。

2. 效果

界面展示效果就不要吐槽了,我们的关注点是功能。

  1. 开始编辑前
    开始编辑前
  2. 编辑后
    编辑后
  3. 复原
    复原

3. 使用方法

为了将此功能通用,所以这次选择使用WPF里的附加属性的方式。

3.1 XAML文件

<!-- XAML中 --><ComboBox Grid.Row="2" Grid.Column="1" ItemsSource="{Binding SqlTypeList}"  SelectedValue="{Binding SqlType}" behavior:TransactionBehavior.CanRollback="True"/><TextBox Grid.Row="0" Grid.Column="1"  Text="{Binding Name}" behavior:TransactionBehavior.CanRollback="True"/>

3.2 相应的VM

// 对应的VMpublic sealed class ActionTreeNodeViewModel : ObservableObject, IFeedback{        private void instantiateCommand()        {            // 回滚变化            ResetActionRecordCommand = new RelayCommand<ActionTreeNodeViewModel>((currentAtnVm) =>            {                App.Messenger.NotifyColleagues(MessengerToken.TRANSACTION_ROLLBACK, currentAtnVm);            }, (currentAtnVm) =>            {                return true;            });            // 保存编辑的结果            SaveActionRecordCommand = new RelayCommand<ActionTreeNodeViewModel>((currentAtnVm) =>            {                // 去某处获取action原始的name, service原始的id                App.Messenger.NotifyColleagues(MessengerToken.TRANSACTION_RETRIEVAL, currentAtnVm);                App.Messenger.NotifyColleagues(MessengerToken.TRANSACTION_RETRIEVAL, currentAtnVm.Parent);                ServiceFileOperater.SaveActionNode(currentAtnVm, currentAtnVm.Parent.LocationFile.FullPath);                App.Messenger.NotifyColleagues(MessengerToken.TRANSACTION_APPLY, currentAtnVm);            }, (currentAtnVm) =>            {                return true;            });        }        // Interface implement        void IFeedback.Feedback(Dictionary<string, Tuple<object, object>> modifyVals, Object dataContext)        {            if (dataContext != this)            {                return;            }            bool anyChange = false;            foreach (KeyValuePair<String, Tuple<object, object>> keyVal in modifyVals)            {                var oldVal = keyVal.Value.Item1;                var newVal = keyVal.Value.Item2;                if (oldVal is IComparable)                {                    if (((IComparable)oldVal).CompareTo(newVal) != 0)                    {                        anyChange = true;                        break;                    }                }            }            // 本vm的值被修改过            this.IsModified = anyChange;        }        void IFeedback.RetrievalChange(Dictionary<string, Tuple<object, object>> modifyVals, Object dataContext)        {            if (dataContext != this)            {                return;            }            // 我们需要原始name去更新文件            var nameStr = ReflectHelper.GetMemberName((ActionTreeNodeViewModel c) => c.Name);            if (!modifyVals.ContainsKey(nameStr))            {                return;            }            var originValOfName = modifyVals[nameStr].Item1;            this.OriginName = originValOfName.ToString();        }}

4. 相关源码

4.1 IFeedback 接口

// 实现这个接口代表需要反馈相关信息interface IFeedback{    /// <summary>    /// 所有希望得到被改变相关信息的对象都应该实现这个接口    /// </summary>    /// <param name="modifyVals">被改变的字段名, 和改变前的值,改变后的值</param>    /// <param name="dataContext">当前dataContext</param>    void Feedback(Dictionary<String, Tuple<Object, Object>> modifyVals, Object dataContext);    /// <summary>    /// 检索变化, 为了尽量减少TransactionBehavior的关联性, 故增加该方法; 如果有需要检索变化情况来进行某些操作时,实现该方法;     ///  例如我们需要Service, Action的原始唯一性标识去更新文件; 为了不让TransactionBehavior知晓Service,Action的存在.    ///   1. 通过实现该接口, service,action通过发送message来进行orgin主键的更新    /// </summary>    /// <param name="modifyVals">被改变的字段名, 和改变前的值,改变后的值</param>    /// <param name="dataContext">当前dataContext</param>    void RetrievalChange(Dictionary<String, Tuple<Object, Object>> modifyVals,Object dataContext);}

4.2 TransactionBehavior

    // FIXME 这里面有大量 is TextBox类似的判断 !!!    // 可回滚    public sealed class TransactionBehavior    {        // 控件 - 对应的DataContext 组成的键值对        // 所以以Value反查key的话, 我们就能找到以该Value为DataContext的控件        private static Dictionary<Control, Object> controlDataContextDic = new Dictionary<Control, object>();        #region DP        // 原始值        private static DependencyProperty OriginValProperty = DependencyProperty.Register("OriginVal", typeof(Object), typeof(Control), new PropertyMetadata(String.Empty, (deo, args) =>        {            // DependencyObject d, DependencyPropertyChangedEventArgs e        }));        // 是否被编辑过?        private static DependencyProperty IsEditProperty = DependencyProperty.Register("IsEdit", typeof(bool), typeof(Control), new PropertyMetadata(false, (deo, args) =>        {            // DependencyObject d, DependencyPropertyChangedEventArgs e        }));        #endregion        static TransactionBehavior()        {            //            // 应用            App.Messenger.Register<Object>(MessengerToken.TRANSACTION_APPLY, (source) =>            {                foreach (KeyValuePair<Control, Object> keyVal in controlDataContextDic)                {                    // 更新每个控件自身记录的OriginVal                    if (keyVal.Value == source)                    {                        var currentControl = keyVal.Key;                        DependencyProperty dp = getDP(currentControl);                        var name = currentControl.GetBindingExpression(dp).ParentBinding.Path.Path;                        var val = source.GetType().GetProperty(name).GetValue(source, null);                        currentControl.SetValue(OriginValProperty, val);                        currentControl.Background = System.Windows.Media.Brushes.White;                    }                }                checkIfItemOfDataContextChanged(source);            });            // 回滚            App.Messenger.Register<Object>(MessengerToken.TRANSACTION_ROLLBACK, (source) =>            {                foreach (KeyValuePair<Control, Object> keyVal in controlDataContextDic)                {                    // 回滚每个控件自身记录的OriginVal                    if (keyVal.Value == source)                    {                        var currentControl = keyVal.Key;                        setControlValue(currentControl, currentControl.GetValue(OriginValProperty));                    }                }                checkIfItemOfDataContextChanged(source);            });            // 检索原始值            App.Messenger.Register<Object>(MessengerToken.TRANSACTION_RETRIEVAL, (source) =>            {                if (!(source is IFeedback))                {                    return;                }                var context = retrievalChanged(source);                ((IFeedback)source).RetrievalChange(context, source);            });        }        public static bool GetCanRollback(DependencyObject obj)        {            return (bool)obj.GetValue(CanRollbackProperty);        }        public static void SetCanRollback(DependencyObject obj, bool value)        {            obj.SetValue(CanRollbackProperty, value);        }        // Using a DependencyProperty as the backing store for CanRollback.  This enables animation, styling, binding, etc...        public static readonly DependencyProperty CanRollbackProperty =            DependencyProperty.RegisterAttached("CanRollback", typeof(bool), typeof(TransactionBehavior), new PropertyMetadata(false, OnValueChanged));        private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)        {            // 设置本控件最初始的值            setOriginVal(d);            // 挂接值改变时的事件            onValChangedEvent(d);            // 记录控件及其DataContext的对应关系            controlDataContextDic.Add((Control)d, ((Control)d).DataContext);            onDataContextChanged(d);            //((SimpleDO)d).OnValueChanged(e);        }        private static void setOriginVal(DependencyObject d)        {            System.Windows.Data.BindingExpression be = ((Control)d).GetBindingExpression(getDP(((Control)d)));            if (null == be)            {                return;            }            // 设置初始值            var binding = be.ParentBinding;            // source为 datacontext            // binding.Source 这个Source就是binding里设置的哪个            var source = be.DataItem;            var path = binding.Path;            var pi = source.GetType().GetProperty(path.Path);            //if (null == pi)            //{            //    d.SetValue(OriginValProperty, "");            //}            //else            //{            // https://stackoverflow.com/questions/3531318/convert-changetype-fails-on-nullable-types            var pType = pi.PropertyType;            pType = Nullable.GetUnderlyingType(pType) ?? pType;            var pVal = pi.GetValue(source, null);            var value = pVal == null ? null : Convert.ChangeType(pVal, pType);            d.SetValue(OriginValProperty, value);            //}        }        private static void onValChangedEvent(DependencyObject d)        {            if (d is TextBox)            {                ((TextBox)d).TextChanged += (sender, args) =>                {                    // object sender, TextChangedEventArgs e                    // var sou = args.Source; // 一般情况下等于sender                    // 给出该控件内容已被修改                    var tbCopy = ((TextBox)sender);                    var changes = args.Changes;                    var currentTxt = tbCopy.Text;                    var originVal = ((TextBox)sender).GetValue(OriginValProperty);                    if (originVal != null && currentTxt.Equals(originVal.ToString()))                    {                        tbCopy.SetValue(IsEditProperty, false);                        tbCopy.Background = System.Windows.Media.Brushes.White;                    }                    else                    {                        tbCopy.SetValue(IsEditProperty, true);                        tbCopy.Background = System.Windows.Media.Brushes.Red;                    }                    checkIfItemOfDataContextChanged(tbCopy.DataContext);                };            }            else if (d is ComboBox)            {                ((ComboBox)d).SelectionChanged += (sender, args) =>                {                    // object sender, SelectionChangedEventArgs e                    // var sou = args.Source; // 一般情况下等于sender                    // 提示被修改                    //var changes = args.                    var cbCopy = ((ComboBox)sender);                    var currentVal = cbCopy.SelectedItem;//SelectedValue取到的还是之前的值,而不是被回滚操作设置的值                    var originVal = cbCopy.GetValue(OriginValProperty);                    if (originVal != null && currentVal != null && currentVal.ToString().Equals(originVal.ToString()))                    {                        cbCopy.SetValue(IsEditProperty, false);                        cbCopy.Background = System.Windows.Media.Brushes.White;                    }                    else                    {                        cbCopy.SetValue(IsEditProperty, true);                        cbCopy.Background = System.Windows.Media.Brushes.Red;                    }                    checkIfItemOfDataContextChanged(cbCopy.DataContext);                };            }        }        private static void onDataContextChanged(DependencyObject d)        {            ((Control)d).DataContextChanged += (sender, args) =>            {                // object sender, DependencyPropertyChangedEventArgs e                // 确认之前的记录正确                if (controlDataContextDic[(Control)sender] != args.OldValue)                {                    throw new ArgumentException("之前的DataContext记录混乱!记录的为: [ " + controlDataContextDic[(Control)sender] + "]; 事件回调传递的是: [ " + args.OldValue + " ]");                }                //更新为 控件更换的DataContext                controlDataContextDic.Add((Control)sender, args.NewValue);                // ------ 修改OriginVal                System.Windows.Data.BindingExpression beCopy = ((Control)d).GetBindingExpression(getDP(((Control)d)));                if (null == beCopy)                {                    return;                }                var bindingCopy = beCopy.ParentBinding;                var pathCopy = bindingCopy.Path;                var piCopy = args.NewValue.GetType().GetProperty(pathCopy.Path);                var valueCopy = piCopy.GetValue(args.NewValue, null);                ((Control)sender).SetValue(OriginValProperty, valueCopy);            };        }        private static void setControlValue(Control currentControl, Object val)        {            DependencyProperty dp = getDP(currentControl);            currentControl.SetValue(dp, val);        }        private static void checkIfItemOfDataContextChanged(Object dataContext)        {            if (!(dataContext is IFeedback))            {                return;            }            var context = retrievalChanged(dataContext);            ((IFeedback)dataContext).Feedback(context, dataContext);        }        private static Dictionary<String, Tuple<Object, Object>> retrievalChanged(Object dataContext)        {            Dictionary<String, Tuple<Object, Object>> context = new Dictionary<String, Tuple<Object, Object>>();            foreach (KeyValuePair<Control, Object> keyVal in controlDataContextDic)            {                // 以该dataContext为数据源的控件                if (keyVal.Value == dataContext)                {                    var currentControl = keyVal.Key;                    DependencyProperty dp = getDP(currentControl);                    String name = currentControl.GetBindingExpression(dp).ParentBinding.Path.Path;                    Object currentVal = currentVal = currentControl.GetValue(dp);                    Object originVal = currentControl.GetValue(OriginValProperty);                    if (context.ContainsKey(name))                    {                        context.Remove(name);                    }                    context.Add(name, Tuple.Create<Object, Object>(currentControl.GetValue(OriginValProperty), currentVal));                }            }            return context;        }        private static DependencyProperty getDP(Control currentControl)        {            DependencyProperty dp = null;            if (currentControl is TextBox)            {                dp = TextBox.TextProperty;            }            else if (currentControl is ComboBox)            {                dp = ComboBox.SelectedValueProperty;            }            return dp;        }    }}

4.3 Messenger组件

本解决方案使用了MVVMLight里的Messenger组件;除此之外就没有其它额外的依赖了。对此也在这里一并给出该组件相应的源码。

/// <summary>/// Provides loosely-coupled messaging between/// various colleague objects.  All references to objects/// are stored weakly, to prevent memory leaks./// </summary>public class Messenger{    #region Constructor    public Messenger()    {    }    #endregion // Constructor    #region Register    /// <summary>    /// Registers a callback method, with no parameter, to be invoked when a specific message is broadcasted.    /// </summary>    /// <param name="message">The message to register for.</param>    /// <param name="callback">The callback to be called when this message is broadcasted.</param>    public void Register(string message, Action callback)    {        this.Register(message, callback, null);    }    /// <summary>    /// Registers a callback method, with a parameter, to be invoked when a specific message is broadcasted.    /// </summary>    /// <param name="message">The message to register for.</param>    /// <param name="callback">The callback to be called when this message is broadcasted.</param>    public void Register<T>(string message, Action<T> callback)    {        this.Register(message, callback, typeof(T));    }    void Register(string message, Delegate callback, Type parameterType)    {        if (String.IsNullOrEmpty(message))            throw new ArgumentException("'message' cannot be null or empty.");        if (callback == null)            throw new ArgumentNullException("callback");        this.VerifyParameterType(message, parameterType);        _messageToActionsMap.AddAction(message, callback.Target, callback.Method, parameterType);    }    [Conditional("DEBUG")]    void VerifyParameterType(string message, Type parameterType)    {        Type previouslyRegisteredParameterType = null;        if (_messageToActionsMap.TryGetParameterType(message, out previouslyRegisteredParameterType))        {            if (previouslyRegisteredParameterType != null && parameterType != null)            {                if (!previouslyRegisteredParameterType.Equals(parameterType))                    throw new InvalidOperationException(string.Format(                        "The registered action's parameter type is inconsistent with the previously registered actions for message '{0}'.\nExpected: {1}\nAdding: {2}",                        message,                        previouslyRegisteredParameterType.FullName,                        parameterType.FullName));            }            else            {                // One, or both, of previouslyRegisteredParameterType or callbackParameterType are null.                if (previouslyRegisteredParameterType != parameterType)   // not both null?                {                    throw new TargetParameterCountException(string.Format(                        "The registered action has a number of parameters inconsistent with the previously registered actions for message \"{0}\".\nExpected: {1}\nAdding: {2}",                        message,                        previouslyRegisteredParameterType == null ? 0 : 1,                        parameterType == null ? 0 : 1));                }            }        }    }    #endregion // Register    #region NotifyColleagues    /// <summary>    /// Notifies all registered parties that a message is being broadcasted.    /// </summary>    /// <param name="message">The message to broadcast.</param>    /// <param name="parameter">The parameter to pass together with the message.</param>    public void NotifyColleagues(string message, object parameter)    {        if (String.IsNullOrEmpty(message))            throw new ArgumentException("'message' cannot be null or empty.");        Type registeredParameterType;        if (_messageToActionsMap.TryGetParameterType(message, out registeredParameterType))        {            if (registeredParameterType == null)                throw new TargetParameterCountException(string.Format("Cannot pass a parameter with message '{0}'. Registered action(s) expect no parameter.", message));        }        var actions = _messageToActionsMap.GetActions(message);        if (actions != null)            actions.ForEach(action => action.DynamicInvoke(parameter));    }    /// <summary>    /// Notifies all registered parties that a message is being broadcasted.    /// </summary>    /// <param name="message">The message to broadcast.</param>    public void NotifyColleagues(string message)    {        if (String.IsNullOrEmpty(message))            throw new ArgumentException("'message' cannot be null or empty.");        Type registeredParameterType;        if (_messageToActionsMap.TryGetParameterType(message, out registeredParameterType))        {            if (registeredParameterType != null)                throw new TargetParameterCountException(string.Format("Must pass a parameter of type {0} with this message. Registered action(s) expect it.", registeredParameterType.FullName));        }        var actions = _messageToActionsMap.GetActions(message);        if (actions != null)            actions.ForEach(action => action.DynamicInvoke());    }    #endregion // NotifyColleauges    #region MessageToActionsMap [nested class]    /// <summary>    /// This class is an implementation detail of the Messenger class.    /// </summary>    private class MessageToActionsMap    {        #region Constructor        internal MessageToActionsMap()        {        }        #endregion // Constructor        #region AddAction        /// <summary>        /// Adds an action to the list.        /// </summary>        /// <param name="message">The message to register.</param>        /// <param name="target">The target object to invoke, or null.</param>        /// <param name="method">The method to invoke.</param>        /// <param name="actionType">The type of the Action delegate.</param>        internal void AddAction(string message, object target, MethodInfo method, Type actionType)        {            if (message == null)                throw new ArgumentNullException("message");            if (method == null)                throw new ArgumentNullException("method");            lock (_map)            {                if (!_map.ContainsKey(message))                    _map[message] = new List<WeakAction>();                _map[message].Add(new WeakAction(target, method, actionType));            }        }        #endregion // AddAction        #region GetActions        /// <summary>        /// Gets the list of actions to be invoked for the specified message        /// </summary>        /// <param name="message">The message to get the actions for</param>        /// <returns>Returns a list of actions that are registered to the specified message</returns>        internal List<Delegate> GetActions(string message)        {            if (message == null)                throw new ArgumentNullException("message");            List<Delegate> actions;            lock (_map)            {                if (!_map.ContainsKey(message))                    return null;                List<WeakAction> weakActions = _map[message];                actions = new List<Delegate>(weakActions.Count);                for (int i = weakActions.Count - 1; i > -1; --i)                {                    WeakAction weakAction = weakActions[i];                    if (weakAction == null)                        continue;                    Delegate action = weakAction.CreateAction();                    if (action != null)                    {                        actions.Add(action);                    }                    else                    {                        // The target object is dead, so get rid of the weak action.                        weakActions.Remove(weakAction);                    }                }                // Delete the list from the map if it is now empty.                if (weakActions.Count == 0)                    _map.Remove(message);            }            // Reverse the list to ensure the callbacks are invoked in the order they were registered.            actions.Reverse();            return actions;        }        #endregion // GetActions        #region TryGetParameterType        /// <summary>        /// Get the parameter type of the actions registered for the specified message.        /// </summary>        /// <param name="message">The message to check for actions.</param>        /// <param name="parameterType">        /// When this method returns, contains the type for parameters         /// for the registered actions associated with the specified message, if any; otherwise, null.        /// This will also be null if the registered actions have no parameters.        /// This parameter is passed uninitialized.        /// </param>        /// <returns>true if any actions were registered for the message</returns>        internal bool TryGetParameterType(string message, out Type parameterType)        {            if (message == null)                throw new ArgumentNullException("message");            parameterType = null;            List<WeakAction> weakActions;            lock (_map)            {                if (!_map.TryGetValue(message, out weakActions) || weakActions.Count == 0)                    return false;            }            parameterType = weakActions[0].ParameterType;            return true;        }        #endregion // TryGetParameterType        #region Fields        // Stores a hash where the key is the message and the value is the list of callbacks to invoke.        readonly Dictionary<string, List<WeakAction>> _map = new Dictionary<string, List<WeakAction>>();        #endregion // Fields    }    #endregion // MessageToActionsMap [nested class]    #region WeakAction [nested class]    /// <summary>    /// This class is an implementation detail of the MessageToActionsMap class.    /// </summary>    private class WeakAction    {        #region Constructor        /// <summary>        /// Constructs a WeakAction.        /// </summary>        /// <param name="target">The object on which the target method is invoked, or null if the method is static.</param>        /// <param name="method">The MethodInfo used to create the Action.</param>        /// <param name="parameterType">The type of parameter to be passed to the action. Pass null if there is no parameter.</param>        internal WeakAction(object target, MethodInfo method, Type parameterType)        {            if (target == null)            {                _targetRef = null;            }            else            {                _targetRef = new WeakReference(target);            }            _method = method;            this.ParameterType = parameterType;            if (parameterType == null)            {                _delegateType = typeof(Action);            }            else            {                _delegateType = typeof(Action<>).MakeGenericType(parameterType);            }        }        #endregion // Constructor        #region CreateAction        /// <summary>        /// Creates a "throw away" delegate to invoke the method on the target, or null if the target object is dead.        /// </summary>        internal Delegate CreateAction()        {            // Rehydrate into a real Action object, so that the method can be invoked.            if (_targetRef == null)            {                return Delegate.CreateDelegate(_delegateType, _method);            }            else            {                try                {                    object target = _targetRef.Target;                    if (target != null)                        return Delegate.CreateDelegate(_delegateType, target, _method);                }                catch                {                }            }            return null;        }        #endregion // CreateAction        #region Fields        internal readonly Type ParameterType;        readonly Type _delegateType;        readonly MethodInfo _method;        readonly WeakReference _targetRef;        #endregion // Fields    }    #endregion // WeakAction [nested class]    #region Fields    readonly MessageToActionsMap _messageToActionsMap = new MessageToActionsMap();    #endregion // Fields}

4. 总结

转投Java这么久了,但回首时发现在看过Java里看的那些源码之后,之前.NET里那些对于我而言模糊晦涩的概念不知不觉中变得无比清晰。

原创粉丝点击