不做開篇廢話,我們發現: AdaptiveTrigger 不夠好 我們知道,UWP可以在一個頁面適應不同尺寸比例的屏幕。一般來說這個功能是通過官方推薦的AdaptiveTrigger 進行的。 比如這樣: 我們可以看到這樣的的Trigger制定了最小值,隱含了條件“當滿足長寬都大於於這個條件時,這個 ...
不做開篇廢話,我們發現:
AdaptiveTrigger 不夠好
我們知道,UWP可以在一個頁面適應不同尺寸比例的屏幕。一般來說這個功能是通過官方推薦的AdaptiveTrigger 進行的。
比如這樣:
<VisualState x:Name="NarrowView">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="800" MinWindowHeight="600"/>
</VisualState.StateTriggers>
</VisualState>
我們可以看到這樣的的Trigger制定了最小值,隱含了條件“當滿足長寬都大於於這個條件時,這個狀態會被觸發;但如果有更嚴格的條件被觸發,那麼優先觸發更嚴格的那個狀態"
這聽上去是個高大上,暗含模式匹配概念的好主意。但是如果你對其中的條件哪怕有一點拿不住,這樣的trigger往往會造成開發中的混亂。
AdaptiveTrigger模糊的命中規則
比如下麵的例子
例:
A:
<AdaptiveTrigger MinWindowWidth="800" MinWindowHeight="600"/>
B:
<AdaptiveTrigger MinWindowWidth="600" MinWindowHeight="800"/>
這兩個貨究竟誰會優先被觸發?你得手動實驗。
而且由於你不知道到Windows Runtime內部是怎麼管理這些小玩意(本地代碼),有時候你只需要簡單的通過長寬比較判斷的橫豎屏切換竟然要燒腦一番。再加上VisualState元素里往往含有巨多的動畫和屬性設置,我們很難將所有的Trigger拉到一起進行有效的管理,這對頁面的構建可能會產生很大的阻礙。
AdaptiveTrigger只訂閱視窗大小切換VisualState是不夠的
當視窗的大小不敏感,對窗體內部的一些元素大小敏感的時候,只針對視窗的大小監視顯然也是不夠的,我們需要更多的邏輯擴展。
Page | ||
Content(Size變化無法被AdaptiveTrigger訂閱) | ||
比如我有一個頁面“Page”,裡面有一個從伺服器端獲取內容的控制項“Content”。這時候我設置了兩個VisualStateGroup: PageLayoutGroup 和 ContentLayoutGroup,分別應付外層Page的長寬變化,和內部Content的長寬變化(內容要訪問伺服器Render到界面以後才會知道Size). 這時候想用AdaptiveTrigger來控制ContentLayoutGroup 的State切換就玩不轉了。
我們需要針對任意控制項的屬性監控來切換State
於是你可能想實現一個自己的StateTrigger。這時候,更大的坑出現了,我們來分析。
自定義StateTrigger的限制與風險
我們來看一個我Appconsult同事跟我們一起討論用的例子代碼:
public class SizeTrigger : StateTriggerBase
{
public SizeTrigger()
{
Window.Current.SizeChanged += Current_SizeChanged;
}
private void Current_SizeChanged(object sender, WindowSizeChangedEventArgs e)
{
Debug.WriteLine($"CurrentSize_Changed: {DateTime.Now}");
SizeObject _size = new SizeObject();
_size.width = e.Size.Width;
_size.height = e.Size.Height;
dynamic result = SetTrigger(Orientation, _size);
SetActive(result);
}
}
限制1:無法精確控制生存期
這位同事說第一次他首先想到這樣做。當然他知道這裡可以用WeakEventListener,但是測試嘛,先跑通看看。
但是他發現"OMG 為什麼我都navigate到 Page2了這個事件還會觸發到這個實例來"。
就算實現了WeakEventListener,無法控制生存期,能免得了MemoryLeak 免不了Exception啊。難道要加大號的TryCatch?那也是個消耗內!
後來他實測了WeakEventListener,果然開始一段時間事件還是丟到了未註銷的訂閱裡面。然後他發現了新bug:當NavigationCacheMode 打開的時候 ,當離開這個頁面到page2 一段時間再回來這個頁面,弱引用已經被釋放掉了,訂閱size的功能被取消了,沒有重新新訂閱的機會。
所以, StateTriggerBase 沒有提供明確的Onload/OnUnload 生存期註入點,成為這一類擴展的巨大限制。
於是我們順理成章的建議,我們可以綁控制項,不綁Window.Current嘛,
"給你的SizeTrigger加一個Panel屬性 綁定到你得RootGrid上面,你訂閱這貨怎麼樣?"
結果遇到了第二個限制:
限制2:在VisualGroup屬性內產生的Trigger,綁定其他元素經常失效或造成Xaml設計器崩潰
這點老司機們往往會有體會,當你聲明對象的父節點有一層或者基層不是DP/FrameworkElements的時候,運行時可能無法得到正確的綁定上下文(在某幾個版本的Windows Runtime出現過 我沒有Check 最新版本) 當SizeTrigger拿不到綁定值的時候,SizeTrigger是無法訂閱目標變化的。
同時我也提出提出第三個不爽的地方:
限制3:分散的邏輯仍然難以整體控制
相關的非此即彼的幾個Trigger,把他們寫成若幹個邏輯分散的Trigger實現,還要他們分別埋在不同的State裡面,生產力提高了嗎?
這時候我就拿 Greater Share的代碼出來給他們看我的behavior方案了
用Behavior解決問題
不是我藏私,是我寫Greater Share代碼的時候覺得分散管理生產力低下,一周前就寫了一個自用,誰想到擴展StaeTriggerBase會有那麼多坑啊(逃
Behavior設計思路
實現我們的Behavior首先要利用下麵兩個類型的特性
- Behavior
- 綁定友好,能夠拿到各種綁定上下文
- 具有完整的 OnAttach/OnDetatch 生存期支持
- 能夠附加在任何DepenedencyObject上 獲取其狀態和事件。
- StateTrigger
- 不含邏輯,簡單根據屬性的True/False進行判斷是否命中
我原本就是為了生產力來設計這個Behavior。
思路是:
如果分散的邏輯很麻煩,我幹啥不設計一個超然的管理器來管理多個StateTrigger呢?
集中控制,我讓誰上誰就上。
這樣一想就會發現,AdaptiveTrigger也一定有一個傀儡師在操控吧?
Behavior運行流程:
- 獲取監視目標
- 獲取可以操控的StateTrigger
- 訂閱監視目標感興趣值的變化
- 根據值判斷哪個State更合適,用代碼激活它
代碼
大概是這個樣子
public class StateTriggerActiveReadingBehavior : Behavior<Panel>
{
long NarrowTriggerPropertyReg;
long WideTriggerPropertyReg;
protected override void OnAttached() //訂閱
{
AssociatedObject.SizeChanged += AssociatedObject_SizeChanged;
NarrowTriggerPropertyReg = RegisterPropertyChangedCallback(NarrowTriggerProperty, (o, a) => RefreshState());
WideTriggerPropertyReg = RegisterPropertyChangedCallback(WideTriggerProperty, (o, a) => RefreshState());
base.OnAttached();
}
private void AssociatedObject_SizeChanged(object sender, SizeChangedEventArgs e)
{
RefreshState();
}
private void RefreshState() //判斷條件,選一個Trigger狀態來激活。
{
//if (true)
//{
// WideTrigger.IsActive = false;
// NarrowTrigger.IsActive = true;
//}
}
protected override void OnDetaching() //註銷
{
base.OnDetaching();
AssociatedObject.SizeChanged -= AssociatedObject_SizeChanged;
UnregisterPropertyChangedCallback(NarrowTriggerProperty, NarrowTriggerPropertyReg);
UnregisterPropertyChangedCallback(WideTriggerProperty, WideTriggerPropertyReg);
}
public StateTrigger NarrowTrigger //窄狀態
{
get { return (StateTrigger)GetValue(NarrowTriggerProperty); }
set { SetValue(NarrowTriggerProperty, value); }
}
public static readonly DependencyProperty NarrowTriggerProperty =
DependencyProperty.Register(nameof(NarrowTrigger), typeof(StateTrigger), typeof(StateTriggerActiveReadingBehavior), new PropertyMetadata(null));
public StateTrigger WideTrigger //寬狀態
{
get { return (StateTrigger)GetValue(WideTriggerProperty); }
set { SetValue(WideTriggerProperty, value); }
}
public static readonly DependencyProperty WideTriggerProperty =
DependencyProperty.Register(nameof(WideTrigger), typeof(StateTrigger), typeof(StateTriggerActiveReadingBehavior), new PropertyMetadata(null));
}
調用的時候只需綁定兩個屬性就可以了
<Interactivity:Interaction.Behaviors>
<Glue:StateTriggerActiveReadingBehavior
x:Name="StateTriggerActiveReadingBehavior"
WideTrigger="{Binding ElementName=wideTrigger}"
NarrowTrigger="{Binding ElementName=narrowTrigger}"/>
</Interactivity:Interaction.Behaviors>
可以規避那麼多坑是我始料未及的,我們來Review一下我們剛纔提到的各種問題
- AdaptiveTrigger模糊命中不確定 (完美規避)
- AdaptiveTrigger不能訂閱任意來源的狀態變化 (完美規避)
- CustomeTrigger生存期不能控制,容易造成未捕獲異常和記憶體泄漏(完美規避)
- CustomeTrigger綁定不便或容易造成異常(完美規避)
- CustomeTrigger邏輯分散不集中造成生產力低下(完美規避)
此外利用了綁定技術還降低了另一種“GotToStateActionBehavior”方案對於Magic String名稱的依賴,似乎還不錯?
希望這種VisualState的控制模式對大家的開發有所啟發幫助。
另外完整的代碼在這裡