前言
因为本身对wpf理解不深如果有技术方面的问题希望可以帮忙纠正,谢谢。
1.MVVM是什么
从百度百科(MVVM_百度百科)可以了解它出现的原因以及优点。它的其中一个优点就是低耦合,这篇文章也是想聊聊.net是如何对Model-View解耦的,因为.net的代码比较复杂,现在只拆出来关键代码来描述。
2.Wpf中的Binding
熟悉wpf的朋友都知道wpf中大部分控件都继承自FrameworkElement,FrameworkElement中有一个方法SetBinding
public BindingExpressionBase SetBinding(DependencyProperty dp, BindingBase binding)
{
return BindingOperations.SetBinding(this, dp, binding);
}
这个方法有两个参数DependencyProperty和BindingBase以及一个返回值BindingExpressionBase,我们简单看下这3个类
(1)DependencyProperty wpf中的依赖属性 例如:TextBox.TextProperty
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(TextBox), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault | FrameworkPropertyMetadataOptions.Journal, OnTextPropertyChanged, CoerceText, isAnimationProhibited: true, UpdateSourceTrigger.LostFocus));
public string Text
{
get
{
return (string)GetValue(TextProperty);
}
set
{
SetValue(TextProperty, value);
}
}
(2)BindingBase wpf中数据绑定的基类,经常使用的Binding就是BindingBase的子类,BindingBase中有一个内部的方法CreateBindingExpression,CreateBindingExpression中直接调用了抽象方法CreateBindingExpressionOverride,主要代码如下
internal BindingExpressionBase CreateBindingExpression(DependencyObject targetObject, DependencyProperty targetProperty)
{
_isSealed = true;
return CreateBindingExpressionOverride(targetObject, targetProperty, null);
}
internal abstract BindingExpressionBase CreateBindingExpressionOverride(DependencyObject targetObject, DependencyProperty targetProperty, BindingExpressionBase owner);
Binding继承BindingBase重写了CreateBindingExpressionOverride
internal override BindingExpressionBase CreateBindingExpressionOverride(DependencyObject target, DependencyProperty dp, BindingExpressionBase owner)
{
return BindingExpression.CreateBindingExpression(target, dp, this, owner);
}
可以看到CreateBindingExpressionOverride的返回值为BindingExpressionBase,等下再说这两个方法,先介绍BindingExpressionBase
(3)BindingExpressionBase 从字面意思可以理解为Binding的表达式,实际上就是存储Binding的值的关系(谁绑定了谁)
可以看出SetBinding时将控件的依赖属性和要Binding的数据做了一个关系,即BindingExpressionBase,我们知道一个正常的通知要么用委托要么用事件将两个需要相互影响的值关联起来。下面我列举2种形式:
(1)通知方/被通知方关联目标对象
public class TestVM
{
public event Action<string> VMChanged;
private string vM;
public string VM
{
get => vM;
set
{
vM = value;
VMChanged?.Invoke(vM);
}
}
}
public class MyButton : Button
{
public MyButton()
{
}
public MyButton(TestVM testVM)
{
testVM.VMChanged += TestVM_VMChanged;
}
private void TestVM_VMChanged(string data)
{
this.Content = data;
}
}
如上方代码所示,可以在通知方增加属性改变事件,在被通知方构造时订阅这个改变事件从而实现值改变,但是这样做会有一个明显的缺点TestVM和MyButton出现和耦合,MyButton需要有一个方法将TestVM传递到类内部,此时MyButton订阅了TestVM的VMChanged事件,那么假如TestVM一直没被回收,MyButton就一直有一个TestVM的引用导致MyButton无法被回收
(2)借助第3个类
public class Main
{
private MyButton _testButton;
private TestVM _testVM;
public void Init()
{
_testButton = new MyButton();
_testVM = new TestVM();
_testVM.VMChanged += TestVM_VMChanged;
}
private void TestVM_VMChanged(string data)
{
if (_testButton != null)
_testButton.Content = data;
}
public void DisposedTestButton()
{
_testButton = null;
}
public void DisposedTestTestVM()
{
_testVM = null;
}
}
如上方代码所示,借助第3个类Main来将TestVM和MyButton关联起来,这样的话TestVM和MyButton直接就没有了直接联系,我们单方面去调用DisposedTestButton或者DisposedTestTestVM都是可以将他们回收掉的,这也是我们日常开发所用到最多的处理方式,将两个需要相互或者单方向通知的对象之间的关系转移到第3个类(主页面),当然Binding也是采用这种处理方式,不过由于Binding需要更通用且需要自动回收所以做了一些额外处理
3.INotifyPropertyChanged
下面时INotifyPropertyChanged接口的源码
public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler PropertyChanged;
}
我们日常的用法和MvvmLight中的ObservableObject的用法是一致的,下面贴出ObservableObject的部分源码以及我们的日常实现
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public virtual void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class TestVM : INotifyPropertyChanged
{
public event Action<string> VMChanged;
private string vM;
public string VM
{
get => vM;
set
{
vM = value;
VMChanged?.Invoke(vM);
NotifyPropertyChanged("VM");
}
}
#region 实现接口
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
当然只是这样还不够,还需要在前台xaml中对他们进行绑定,在将MyButton_Test的上下文设置为指定的对象(TestVM),就可以实现TestVM和MyButton的相互通知了
<local:MyButton x:Name="MyButton_Test" Content="{Binding VM}"/>
不过我们知道两个事物不可能无缘无故的联系起来,那么必定在Binding的时候做了一些处理,将两个对象关联起来了,下面看源码,由于源码较长只截取关键路径
public sealed class BindingExpression : BindingExpressionBase, IDataBindEngineClient, IWeakEventListener
{
private BindingWorker Worker => _worker;
private void CreateWorker()
{
Invariant.Assert(Worker == null, "duplicate call to CreateWorker");
_worker = new ClrBindingWorker(this, base.Engine);
}
public override void UpdateTarget()
{
if (base.IsDetached)
{
throw new InvalidOperationException(SR.Get("BindingExpressionIsDetached"));
}
if (Worker != null)
{
Worker.RefreshValue();
}
}
}
internal class ClrBindingWorker : BindingWorker
{
private PropertyPathWorker PW => _pathWorker;
internal override void RefreshValue()
{
PW.RefreshValue();
}
}
internal sealed class PropertyPathWorker : IWeakEventListener
{
internal void RefreshValue()
{
for (int i = 1; i < _arySVS.Length; i++)
{
object reference = BindingExpressionBase.GetReference(_arySVS[i].item);
if (!ItemsControl.EqualsEx(reference, RawValue(i - 1)))
{
UpdateSourceValueState(i - 1, null);
return;
}
}
UpdateSourceValueState(Length - 1, null);
}
private void UpdateSourceValueState(int k, ICollectionView collectionView)
{
UpdateSourceValueState(k, collectionView, BindingExpression.NullDataItem, isASubPropertyChange: false);
}
private void UpdateSourceValueState(int k, ICollectionView collectionView, object newValue, bool isASubPropertyChange)
{
//...省略前方代码
for (k++; k < _arySVS.Length; k++)
{
isASubPropertyChange = false;
ICollectionView collectionView2 = _arySVS[k].collectionView;
obj = ((newValue == BindingExpression.NullDataItem) ? RawValue(k - 1) : newValue);
newValue = BindingExpression.NullDataItem;
if (obj == AsyncRequestPending)
{
_status = PropertyPathStatus.AsyncRequestPending;
break;
}
if (!flag && obj == BindingExpressionBase.DisconnectedItem && _arySVS[k - 1].info == FrameworkElement.DataContextProperty)
{
flag = true;
}
ReplaceItem(k, BindingExpression.NullDataItem, obj);
ICollectionView collectionView3 = _arySVS[k].collectionView;
if (collectionView2 != collectionView3 && _host != null)
{
_host.ReplaceCurrentItem(collectionView2, collectionView3);
}
}
//...省略后方代码
}
private void ReplaceItem(int k, object newO, object parent)
{
//...省略前方代码
if (IsDynamic && SVI[k].type != SourceValueType.Direct)
{
Engine.RegisterForCacheChanges(newO, svs.info);
PropertyPath.DowncastAccessor(svs.info, out DependencyProperty dp2, out PropertyInfo pi2, out PropertyDescriptor pd2, out DynamicObjectAccessor _);
INotifyPropertyChanged source2;
if (newO == BindingExpression.StaticSource)
{
Type type2 = (pi2 != null) ? pi2.DeclaringType : pd2?.ComponentType;
if (type2 != null)
{
StaticPropertyChangedEventManager.AddHandler(type2, OnStaticPropertyChanged, SVI[k].propertyName);
}
}
else if (dp2 != null)
{
_dependencySourcesChanged = true;
}
else if ((source2 = (newO as INotifyPropertyChanged)) != null)
{
PropertyChangedEventManager.AddHandler(source2, OnPropertyChanged, SVI[k].propertyName);
}
else if (pd2 != null && newO != null)
{
ValueChangedEventManager.AddHandler(newO, OnValueChanged, pd2);
}
}
//...省略后方代码
}
}
public class PropertyChangedEventManager : WeakEventManager
{
protected override void StartListening(object source)
{
INotifyPropertyChanged notifyPropertyChanged = (INotifyPropertyChanged)source;
notifyPropertyChanged.PropertyChanged += OnPropertyChanged;
}
protected override void StopListening(object source)
{
INotifyPropertyChanged notifyPropertyChanged = (INotifyPropertyChanged)source;
notifyPropertyChanged.PropertyChanged -= OnPropertyChanged;
}
public static void AddHandler(INotifyPropertyChanged source, EventHandler<PropertyChangedEventArgs> handler, string propertyName)
{
if (handler == null)
{
throw new ArgumentNullException("handler");
}
CurrentManager.PrivateAddHandler(source, handler, propertyName);
}
private void PrivateAddHandler(INotifyPropertyChanged source, EventHandler<PropertyChangedEventArgs> handler, string propertyName)
{
AddListener(source, propertyName, null, handler);
}
private void AddListener(INotifyPropertyChanged source, string propertyName, IWeakEventListener listener, EventHandler<PropertyChangedEventArgs> handler)
{
using (base.WriteLock)
{
//...省略前方代码
HybridDictionary hybridDictionary = (HybridDictionary)base[source];
if (hybridDictionary == null)
{
hybridDictionary = (HybridDictionary)(base[source] = new HybridDictionary(caseInsensitive: true));
StartListening(source);
}
//...省略后方代码
}
}
private void OnPropertyChanged(object sender, PropertyChangedEventArgs args)
{
//...省略前方代码
base.DeliverEventToList(sender, args, listenerList);
//...省略后方代码
}
}
public abstract class WeakEventManager : DispatcherObject
{
protected void DeliverEventToList(object sender, EventArgs args, WeakEventManager.ListenerList list)
{
bool flag = list.DeliverEvent(sender, args, base.GetType());
if (flag)
{
this.ScheduleCleanup();
}
}
public override bool DeliverEvent(object sender, EventArgs e, Type managerType)
{
TEventArgs e2 = (TEventArgs)((object)e);
bool flag = false;
int i = 0;
int count = base.Count;
while (i < count)
{
WeakEventManager.Listener listener = base.GetListener(i);
if (listener.Target != null)
{
//eventHandler就是PropertyPathWorker中订阅INotifyPropertyChanged.PropertyChanged事件的方法
EventHandler<TEventArgs> eventHandler = (EventHandler<TEventArgs>)listener.Handler;
if (eventHandler != null)
{
eventHandler(sender, e2);
}
else
{
flag |= base.DeliverEvent(ref listener, sender, e, managerType);
}
}
else
{
flag = true;
}
i++;
}
return flag;
}
}
可以看到在BindingExpression在调用UpdateTarget时最终会调用PropertyChangedEventManager的StartListening订阅INotifyPropertyChanged的PropertyChanged事件,至此两个绑定的属性产生了联系,只要我们在模型里面调用PropertyChanged就可是通知到对应需要改变的对象的属性,需要注意的是这里分别有一个重要的类和接口(WeakEventManager和IWeakEventListener),我们可以看到WeakEventManager充当的角色就是上面的Main(第三个类)将两个类关系起来,下面我们实现自己的通知时需要用到,由于具体改变属性的代码量比较大并且控件和UI具体怎么交互也不太了解,之后学习DependencyObject和DependencyProperty再详细介绍
下面是简单的一个调用关系图
4.根据.Net的实现逻辑实现通知
说了这么多我们仿照着.net的实现逻辑自己实现一份,下面先贴代码
public interface IMyWeakEventTest
{
event Action<object, string> PropertyChanged;
}
第一个接口IMyWeakEventTest对应INotifyPropertyChanged
public class MyWeakEventBindingExpTest : IWeakEventListener
{
public MyWeakEventBindingExpTest(DependencyObject dobj, DependencyProperty dp, object obj, string sourceFiled)
{
Target = new WeakReference<DependencyObject>(dobj);
Source = new WeakReference<object>(obj);
DProperty = dp;
SourceFiled = sourceFiled;
}
public DependencyProperty DProperty { get; set; }
public WeakReference<DependencyObject> Target { get; set; }
public WeakReference<object> Source { get; set; }
public string SourceFiled { get; set; }
public object RSource
{
get
{
if (Source.TryGetTarget(out var source))
return source;
return null;
}
}
public DependencyObject RTarget
{
get
{
if (Target.TryGetTarget(out var target))
return (DependencyObject)target;
return null;
}
}
public void UpdateTarget()
{
this.RTarget.SetValue(this.DProperty, this.RSource.GetType().GetProperty(SourceFiled)?.GetValue(this.RSource));
}
public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
var myWeakEventArgsTest = e as MyWeakEventArgsTest;
if (myWeakEventArgsTest == null)
return true;
if (myWeakEventArgsTest.Value != SourceFiled)
return true;
this.UpdateTarget();
return true;
}
}
第二个MyWeakEventBindingExpTest对应BindingExpression和PropertyPathWorker,由于现在只说原理所以将BindingExpression和PropertyPathWorker的功能合并了,这里可以看到Source(数据源)和Target(目标)我们用了WeakReference(弱引用类)目的是为了内存的自动释放
public class MyWeakEventManagerTest : WeakEventManager
{
protected override void StartListening(object source)
{
var weTest = source as IMyWeakEventTest;
if (weTest != null)
weTest.PropertyChanged += WeTest_PropertyChanged;
}
private void WeTest_PropertyChanged(object arg1, string arg2)
{
base.DeliverEvent(arg1, new MyWeakEventArgsTest() { Value = arg2 });
}
protected override void StopListening(object source)
{
var weTest = source as IMyWeakEventTest;
if (weTest != null)
weTest.PropertyChanged -= WeTest_PropertyChanged;
}
public void AddLinstener(object source, IWeakEventListener eventListener)
{
var myBindingExp = eventListener as MyWeakEventBindingExpTest;
MyWeakEventTestExtension.SetWeakListener(myBindingExp.RTarget, myBindingExp);
myBindingExp.UpdateTarget();
this.ProtectedAddListener(source, eventListener);
}
public void RemoveLinstener(object source, IWeakEventListener eventListener)
{
this.ProtectedRemoveListener(source, eventListener);
}
}
第三个MyWeakEventManagerTest对应PropertyChangedEventManager
public class MyWeakEventTestExtension
{
public static readonly DependencyProperty WeakListenerProperty = DependencyProperty.RegisterAttached("WeakListener", typeof(IWeakEventListener), typeof(MyWeakEventTestExtension), new PropertyMetadata(null));
public static void SetWeakListener(DependencyObject element, IWeakEventListener value) => element.SetValue(WeakListenerProperty, value);
public static IWeakEventListener GetWeakListener(DependencyObject element) => (IWeakEventListener)element.GetValue(WeakListenerProperty);
}
第四个是一个扩展类,主要是为了在Target(即控件)存储IWeakEventListener即MyWeakEventBindingExpTest避免MyWeakEventBindingExpTest被回收
上面代码和框架提供的结构基本一致同样用到了WeakEventManager和IWeakEventListene先看类名WeakEvent(弱事件)Manager(管理员),我们都知道弱引用它会在系统需要内存时被回收,WeakEventManager也是同样的,WeakEventManager内部有一个虚方法Purge(清除),会对无用的订阅进行释放,IWeakEventListene就是为WeakEventManager服务的
protected virtual bool Purge(object source, object data, bool purgeAll)
{
bool result = false;
bool flag = purgeAll || source == null;
if (!flag)
{
ListenerList list = (ListenerList)data;
if (ListenerList.PrepareForWriting(ref list) && source != null)
{
Table[this, source] = list;
}
if (list.Purge())
{
result = true;
}
flag = list.IsEmpty;
}
if (flag && source != null)
{
StopListening(source);
if (!purgeAll)
{
Table.Remove(this, source);
result = true;
}
}
return result;
}
5.测试自定义通知,内存分析
我们先简单的搭一个测试例子
后台代码如下
public partial class Window13 : Window
{
#region 私有成员
//索引
private int _index = 1;
//通知模型列表
private List<MyWeakEventTest> _myWeakEventTests = new List<MyWeakEventTest>();
//弱引用管理
private MyWeakEventManagerTest _myWeakEventManagerTest = new MyWeakEventManagerTest();
#endregion
public Window13()
{
InitializeComponent();
Button_TestEdit_0.Click += Button_TestEdit_0_Click;
Button_ClearView.Click += Button_ClearView_Click;
Button_ClearModel.Click += Button_ClearModel_Click;
Button_GC.Click += Button_GC_Click;
Button_Rest.Click += Button_Rest_Click;
reset();
}
#region 控件事件
/// <summary>
/// 重置视图
/// </summary>
private void Button_Rest_Click(object sender, RoutedEventArgs e)
{
reset();
}
/// <summary>
/// GC
/// </summary>
private void Button_GC_Click(object sender, RoutedEventArgs e)
{
GC.Collect();
}
/// <summary>
/// 清除模型即_myWeakEventTests
/// </summary>
private void Button_ClearModel_Click(object sender, RoutedEventArgs e)
{
_myWeakEventTests.Clear();
}
/// <summary>
/// 清除视图即WrapPanel中的TextBox
/// </summary>
private void Button_ClearView_Click(object sender, RoutedEventArgs e)
{
WrapPanel_Default.Children.Clear();
}
/// <summary>
/// 编辑_myWeakEventTests列表中第一个元素的WTest
/// </summary>
private void Button_TestEdit_0_Click(object sender, RoutedEventArgs e)
{
_myWeakEventTests[0].WTest = $"测试修改{_index}";
_index++;
}
#endregion
#region 辅助方法
/// <summary>
/// 重置视图即清除WrapPanel中的TextBox并重新添加
/// </summary>
private void reset()
{
_myWeakEventTests.Clear();
WrapPanel_Default.Children.Clear();
for (int i = 0; i < 30; i++)
{
var myWeakEventTest = new MyWeakEventTest() { WTest = i.ToString() };
MyTextBox textBox = new MyTextBox() { Width = 100, Height = 30, Text = $"a{i}" };
_myWeakEventManagerTest.AddLinstener(myWeakEventTest, new MyWeakEventBindingExpTest(textBox, MyTextBox.TextProperty, myWeakEventTest, "WTest"));
_myWeakEventTests.Add(myWeakEventTest);
WrapPanel_Default.Children.Add(textBox);
}
}
#endregion
}
public class MyWeakEventTest : IMyWeakEventTest
{
private string _wTest;
public string WTest
{
get => _wTest;
set
{
_wTest = value;
PropertyChanged?.Invoke(this, nameof(WTest));
}
}
public event Action<object, string> PropertyChanged;
}
五个功能按钮的功能如字面意思,具体在代码注释中体现,代码创建了一个自定义的弱引用管理类(MyWeakEventManagerTest)以及一个通知模型列表(List<MyWeakEventTest>),窗口加载时向WrapPanel中增加了30个TextBox,并调用了MyWeakEventManagerTest的AddLinstener,这一步相当于SetBinding在AddLinstener时我们将MyWeakEventBindingExpTest设置到TextBox的附加属性WeakListenerProperty上并且更新目标,同时订阅IMyWeakEventTest.PropertyChanged。
这时如果我们点击按钮"测试修改[0]",代码执行取_myWeakEventTests索引为0的值并设置WTest为[$"测试修改{_index}"],经过MyWeakEventManagerTest的中转最终执行了MyWeakEventBindingExpTest.ReceiveWeakEvent,目前MyWeakEventBindingExpTest中保存了Source、Target以及DProperty,同时ReceiveWeakEvent将我们修改的属性名给到了,那么顺理成章的将Target的DProperty修改为新的Source的值。
说了这么多那么怎么证明它们之间没有耦合,下面通过内存变化来分析
此时我们截取下内存快照,可以看到各个对象的计数, MyWeakEventTest(模型Source)、MyTextBox(控件Target)、MyWeakEventBindingExpTest(关系及通知管理)都是30个,和我们创建的相符,这个时候我们点击“清除View”即将MyTextBox移除,然后重新截取内存快照
可以发现MyTextBox和 MyWeakEventBindingExpTest已经被回收,可以通过对象引用差异对比计数变化都为-30
这时点击“重置”验证清除Model即将MyWeakEventTest移除,然后重新截取内存快照
可以看到MyWeakEventTest已经被回收
6.总结
之前看别人的博客,说代码语言不重要重要的是编程逻辑和思想,现在发现越来越是这个味儿了,之后有时间学习DependencyObject和DependencyProperty再分享