时隔多年,居然会写一篇关于.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
// 对应的VM
public 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里那些对于我而言模糊晦涩的概念不知不觉中变得无比清晰。