WPF之修改变色,回滚功能的实现
来源:互联网 发布:dior homme男装知乎 编辑:程序博客网 时间:2024/06/07 09:57
时隔多年,居然会写一篇关于.NET的文章, 而且还是关于WPF的。真是意外!
这里我就不给出相应的demo了;如果有缘人恰好有这个需求并且自己无法成功,就麻烦到时候再私信我吧。
1. 起因
最近在给公司写内部CS程序时,碰到这样一个需求——进行TextBox内容或者ComboBox的内容编辑时,我如何知道该输入/选择框的值被我修改过?另外一个与此紧密相关的就是编辑器里常见的修改回滚功能。因为关系紧密,所以本博客将两者一起进行讲解,节省篇幅,也节省各位看官的时间。
其实这个需求在我做专职WPF开发时就遇到过,当时的解决方案是使用.NET的TypeDescriptionProvider
注解来为VM提供额外的属性。当时因为水平有限,觉得它比较复杂;加之这么多年了也完全忘记了其中的细节(根据这些年的感悟,之前的水平低可以接受,这笔记没做好就不能忍了。我那逝去的年华,唉!)。所以这次干脆换了种实现方式。得益于从事Java之后的这么几年里源码的学习,这次的实现过程中感觉顺手了不少。
2. 效果
界面展示效果就不要吐槽了,我们的关注点是功能。
- 开始编辑前
- 编辑后
- 复原
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里那些对于我而言模糊晦涩的概念不知不觉中变得无比清晰。
阅读全文
0 0
- WPF之修改变色,回滚功能的实现
- JQUERY实现判断鼠标往上滚往下滚的功能。
- Jquery 实现Radion的变色功能
- 实现wpf listview listbox 隔行变色的简单方法
- WPF爬虫之实现下载功能
- Silverlight/WPF 截图保存功能的实现---我得用这个方式试下,把项目里的功能修改下。
- jquery实现table表的隔行变色功能
- WPF:实现Button类的PerformClick功能
- Silverlight/WPF 截图保存功能的实现
- WPF软件正版激活功能的实现
- Spring学习-回滚事务之修改默认回滚类型
- (android之系统)android OTA 的功能实现和修改
- ecmall修改之:立即购买功能的实现
- WPF拖放功能实现
- WPF拖放功能实现
- WPF拖放功能实现
- WPF拖放功能实现
- WPF拖放功能实现
- bikems项目碰到的问题
- 使用Spring DATA JPA进行数据库开发
- 程序员伤不起的 30 岁
- 20171128 单精度的取整
- live-server
- WPF之修改变色,回滚功能的实现
- 转换十六进制数
- 极大似然估计
- 瀑布流布局
- 演绎手风琴效果
- 【学习C++】学习C++ -> 进一步了解函数
- HDOJ1032 The 3n + 1 problem
- Erasing and Winning UVA
- IOS开发中ARC下的assign和weak区别