原文:
在WPF中減少邏輯與UI元素的耦合在WPF中減少邏輯與UI元素的耦合
周銀輝
1, 避免在邏輯中引用界面元素,别把背景資料強加給UI
一個糟糕的案例
比如說主界面上有一個顯示目前任務狀态的标簽label_TaskState,我們會時常更新該标簽以便及時地将任務狀态通知使用者。那麼很糟糕的一種假設是我們的代碼中會到處充斥着這樣的語句段this.label_TaskState .Content = this.GetStateDescription(TaskStates.Busy);(GetStateDescription方法會傳回一段比較友好的描述資訊)
當使用者點選“暫停”按鈕後,我們可能要這樣來這樣更新标簽:
void btn_Pause_Clicked(object sender, RoutedEventArgs e)
{
//do something to pause the task
//update our lab
this.label_TaskState .Content = this.GetStateDescription(TaskStates.Pause);
}
當由于某種原因我們的任務發生了錯誤時,我們可能會這樣:
try
//do something dangerous
catch(MyException e)
this.label_TaskState .Content = this.GetStateDescription(TaskStates.Error);
finally
//…
這樣一來,我們的邏輯代碼無數地方将引用label_TaskState這個UI元素。
現在有一些變化來了:(1)我們覺得使用一段文本來描述任務狀态還是不夠直覺,是以我們決定使用美工提供的一系列漂亮圖示來顯示目前狀态(圖示中也可能含有文字,不過我們不關心)。(2)另外一個面闆上(myPanel2)也要放置一個顯示任務目前任務狀态的标簽label_TaskState2,隻不過其僅僅顯示文字描述就可以了。
那麼我們在這麼糟糕的環境下是不是應該像這樣來修改我們的代碼呢?
首先找出所有引用了label_TaskState的地方(比如有20個)。
然後将Lable類型的label_TaskState控件修改為Image類型的image_TaskState控件。
然後重複地将this.label_TaskState .Content = this.GetStateDescription(TaskStates.Busy);語句替換為this.image_TaskState.Source = this.GetStateImage(TaskStates.Busy);
别忘了每次都要在該語句後追加一條:this.label_TaskState2.Content = this.GetStateDescription(TaskStates.Busy);因為我們增加了一個标簽。
多麼令人上火的程式設計工作啊。
原因是,我們頻繁地引用不穩定的界面元素(label_TaskState),嚴重地将界面和邏輯耦合在了一起,我們采用指派的方式将背景資料(目前狀态資訊)強加給了UI元素。
解決方案:使用Binding,然UI元素從背景“拿”資料
一個簡單的描述是:背景邏輯對前台UI說“要如何展現由前台決定,資料就在這裡,要用就自己來拿吧”
“資料就在這裡”
我們的資料是目前任務的狀态資訊,為了提供給UI元素和背景邏輯使用,我們決定提供一個TheTaskState屬性來跟蹤目前狀态:
public TaskStates TheTaskState
get
{
return (TaskStates)GetValue(TheTaskStateProperty);
}
set
SetValue(TheTaskStateProperty, value);
public static readonly DependencyProperty TheTaskStateProperty =
DependencyProperty.Register("TheTaskState", typeof(TaskStates),
typeof(Window1), new UIPropertyMetadata(TaskStates.Idle));
這樣背景邏輯中要改變任務狀态時隻需要修改TheTaskState屬性就可以了。
“要用就自己來拿吧”
目前台需要向使用者展現該任務狀态時隻需要讀取該屬性,要實時跟蹤就綁定吧。
<Label x:Name="label_TaskState"
Content="{Binding ElementName=windowMain,Path=TheTaskState}" />
“要如何展現由前台決定”
不對,我要展現給使用者的可不是一些枚舉值,而應該是圖檔或文本。
的确如此,是以我們要在綁定中加入轉換器(或者資料模闆,這裡我們使用轉換器):
public class TaskStatesImageConverter:IValueConverter
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
TaskStates state = (TaskStates)value;
return GetImageFromTaskState(state);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
throw new NotImplementedException();
private Image GetImageFromTaskState(TaskStates state)
Image image = new Image();
image.Source = new BitmapImage(new Uri((int)state+".png", UriKind.Relative));
return image;
Content="{Binding ElementName=windowMain,
Path=TheTaskState,
Converter={StaticResource myTaskStatesImageConverter}}" />
這樣一來,我們的背景邏輯沒有去引用UI元素并把資料強加給它,背景關注于如何任務狀态及其更新,前台專注于如何向使用者展現這些資訊。當我們要更換其他展示方式時,隻需更換一下轉換器就可以了。
2,避免邏輯代碼依賴Template中的元素
Template的目的是可更換,如果和邏輯耦合在一起就很有可能在更換Template的時候出現異常,也就是不可更換,模闆就失去了意義。
糟糕的案例1
比如讓我們來打造ScrollBar這樣的控件,如果按照如下的方式來處理使用者點選上ScrollBar兩端的箭頭就會出現問題:在ScrollBar的ControlTemplate的視覺樹的兩端分别是放置一個ToggleButton,以便使用者點選這兩個按鈕可以上下(或左右)翻頁,如何處理使用者的點選事件呢?錯誤的方式是注冊按鈕的點選事件:
private void RepeatButton1_Click(object sender, RoutedEventArgs e)
{
RepeatButton rb = (RepeatButton)sender;
// left(or up) scrolling
private void RepeatButton2_Click(object sender, RoutedEventArgs e)
// right(or down) scrolling
糟糕的案例2
有時會犯這樣的錯誤:本來我們遵守着很多規範地使用ControlTemplate(DataTemplate是一樣的道理)來将邏輯和UI很好的分開了(比如我們打造了一個不錯的CustromControl),但突然發現似乎要在邏輯代碼中引用ControlTemplate視覺樹中的某個元素,然後發現FrameworkTemplate.FindName()可以完成這項工作,便出現了下面這樣的代碼:
<Style TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Grid Margin="5" Name="grid">
<!--someting else-->
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Grid gridInTemplate = (Grid)myButton1.Template.FindName("grid", myButton1);
//do something about the grid
這些都可以完成工作,但我們知道在WPF中,使用者(你的控件使用者,也可能是你自己)是可以定制ControlTemplate的,那麼使用者完全可以将邏輯引用到的這兩個RepeatButton删掉或更換成其它元素,那麼控件必定殘缺甚至異常。如果不允許使用者更改則失去了Template的意義。
解決方案:
經驗是,當你覺得必須對視覺樹中的元素進行事件注冊以便挂接到某個事件處理方法上時,你可以想辦法将方法所實作的功能包裝成Command。比如案例1中,可以将滾動條的上下(或左右)翻頁包裝成形如ScrollBar.LineUpCommand、ScrollBar.LineDownCommand的形式,然後隻需将視覺樹中的表示上下翻頁的元素的Command屬性指定成他們就可以了。若僅僅是元素的某個事件将改變某些元素(或自己)的狀态時,你可以使用Trigger來達到這一目的(也許你需要增加一些Dependency Property來充當Trigger的條件),比如:
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Background" TargetName="Bg" Value="Red "/>
</Trigger>
</ControlTemplate.Triggers>
絕對沒有借口讓邏輯部分引用DataTemplate的元素,可能會有邏輯引用ContorlTemplate中的元素的情況(打造某些CustomControl時),這時你可以使用TemplatePartAttribute來進行辨別。
打造CustomControl時遇到的耦合問題,可以參考這篇文章:
在WPF中自定義控件(3) CustomControl (下)3,總結
總的說來,我們應該為邏輯和UI的解耦而努力,WPF也為我們提供了這樣的機制。上面的例子僅僅是說明了常見的幾種情況,而提供的解決方案也僅供參考,沒有放之四海而皆準的方案,因為這其中涉及到了太多适應于不同情況下的小技巧。但總體而言:資料綁定、Style,Template,Command,Resource等為邏輯和UI的解耦提供了幾條途徑,如果你發現你的邏輯代碼和UI元素嚴重地耦合在了一起而帶來了不少麻煩,那麼可以從上面的幾條途徑入手。另外,寫這篇文字的最主要目的還是引起大家在實際編碼過程中對邏輯和UI的解耦的重視。