理解WPF Stylet中Command="{s:Action 方法名}"的設計與實現
前言
Stylet是我最近很喜歡使用的一個WPF框架,它的很多設計都體現了約定優于配置的思想。因此你會發現使用它非常方便,幾乎不需要任何配置,開箱即用,只需知道它的一些約定即可。
查看Samples中Hello這個例子,只要在xaml中這樣寫:
<Button Command="{s:Action SayHello}">Say Hello</Button>
然后你點擊這個按鈕,就會觸發對應ViewModel中的SayHello方法,使用起來非常簡單方便。這背后Stylet框架做了什么呢?讓我們揭開它的神秘面紗吧!!
ActionExtension
先來看看ActionExtension,它位于Stylet.Xaml命名空間:

s:Action的第一階段是XAML解析,這個階段的核心任務是將XAML標記中的{s:Action SayHello}語法翻譯成CommandAction實例。這個過程由ActionExtension標記擴展類完成,它是整個命令綁定系統的入口點。
當XAML解析器遇到{s:Action SayHello}時,會調用ActionExtension.ProvideValue方法:

HandleDependencyObject方法根據目標屬性的類型進行分支處理:

對于命令綁定場景,CreateCommandAction方法創建CommandAction實例:

rootObject就是具有s:Action的頁面:

View.ActionTarget
現在我們已經找到了View,但是想要觸發的方法是在ViewModel上的,那么就要想辦法找到對應的ViewModel,Stylet中是通過View.ActionTarget這個附加屬性實現的。通過View.ActionTarget附加屬性將ViewModel注入到可視化樹中,使得后續階段能夠找到正確的命令執行目標。
先來看看View.ActionTarget的定義,位置在命名空間Stylet.Xaml下的View類中:

默認值:InitialActionTarget(一個特殊的標記對象)
繼承性:FrameworkPropertyMetadataOptions.Inherits確保屬性值可以沿著可視化樹向下傳遞
當View.Model屬性被設置時,會觸發PropertyChangedCallback,通過ViewManager建立視圖和ViewModel的關聯。

在將View與ViewModel關聯起來的時候,設置了當前View的ActionTarget為對應的ViewModel。
CommandAction

在CreateCommandAction方法中會返回一個CommandAction。

這里是在把當前這個Subject對象里的ActionTargetProperty的值‘拴’到 某個尚未顯式的目標屬性上,而且只讓它從源(Subject)流向目標,不會反向同步。
這里的Subject是Button對象,為什么也能找到ActionTargetProperty這個屬性呢?
由于ActionTargetProperty設置了FrameworkPropertyMetadataOptions.Inherits標志,這個屬性會自動沿著可視化樹向下傳播:
Window (View.ActionTargetProperty)
├── UserControl (繼承Window的View.ActionTargetProperty)
│ ├── Button (繼承UserControl的View.ActionTargetProperty)
│ └── TextBox (繼承UserControl的View.ActionTargetProperty)
└── Grid (繼承Window的View.ActionTargetProperty)
但是現在還沒完成綁定,只是綁定的一端,還需要設置將這個屬性綁定到哪里。

BindingOperations.SetBinding(this, targetProperty, multiBinding);將ActionBase的targetProperty依賴屬性綁定到View.ActionTargetProperty。

你會發現現在并沒有綁定到ViewModel,只是View.ActionTargetProperty的默認值:

由于targetProperty依賴屬性從null到View.InitialActionTarget也會觸發UpdateActionTarget,不過這一次不做任何處理,直接返回:

當設置View.ActionTargetProperty的值為對應的ViewModel時:

就又會觸發UpdateActionTarget方法,這一次拿到了對應的ViewModel:

拿到ViewModel上的方法:

拿到是否可以執行對應命令的屬性:

現在重點來關注一下這里:
Expressions.ConstantExpression targetExpression = Expressions.Expression.Constant(newTarget);
Expressions.MemberExpression propertyAccess = Expressions.Expression.Property(targetExpression, guardPropertyInfo);
this.guardPropertyGetter = Expressions.Expression.Lambda<Func<bool>>(propertyAccess).Compile();
這段代碼在運行時動態編譯一段表達式樹,生成一個無參、返回 bool的委托(Func<bool>),用來快速讀取某個對象的布爾型屬性值。
第一行相當于就是使用這個newTarget對象,第二行就是訪問這個newTarget對象的CanSayHello屬性,相當于newTarget.CanSayHello。
Expression.Lambda<Func<bool>>將表達式樹包裝成一個無參Lambda表達式(形如 () => newTarget.CanSayHello)。
.Compile()把表達式樹編譯成可執行IL ,生成一個靜態緩存的委托Func<bool>。
然后調用this.UpdateCanExecute();觸發CanExecuteChanged事件,就會執行CanExecute方法,在這個方法中就使用到了剛剛生成的獲取newTarget.CanSayHello屬性的委托:

現在是false,命令無法執行:

當我輸入小明的時候就修改了這個屬性值,就會觸發CanExecuteChanged事件:

就又會調用剛剛生成的那個獲取newTarget.CanSayHello屬性的委托:

這一次返回true,命令可以執行。
當我點擊按鈕的時候,如果命令可以執行,就會執行Execute方法

這樣就會執行ViewModel中對應的方法。
以上就是個人對于WPF Stylet中Command="{s:Action SayHello}"的設計與實現的理解。

浙公網安備 33010602011771號