今天看到了老趙的一片博客:編寫一個“綁定友好”的WPF控件
文章里面遇到的問題,蠻有意思,后面的評論,非常精彩,沒看過的,推薦看一下。
由於我也做過類似的需求,所以,貼出我當時的做法和現在的想法,僅僅是筆記,沒有其他意思。
當時的需求類似這樣子,實際做的效果當然比這個漂亮的多。
看到這個UI的第一反應就是,封裝一個控件,把slider包進去,很簡單的吧。
當時的做法,在CS代碼里面封裝幾個DP,綁定就完事兒了呀。
<UserControl x:Class="WpfControlTest.UCSlider" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="250" Height="45" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" x:Name="main" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" > <Grid> <TextBlock Text="{Binding ElementName=main,Path=Text}" Margin="0,0,0,0" Foreground="Black" VerticalAlignment="Top" HorizontalAlignment="Left"/> <TextBlock Text="{Binding ElementName=sl,Path=Value,StringFormat=0}" Foreground="Black" HorizontalAlignment="Right"/> <Slider Name="sl" Width="{Binding Path=Width,ElementName=main}" VerticalAlignment="Bottom" Cursor="Hand" Maximum="{Binding ElementName=main,Path=Maximum}" Minimum="{Binding ElementName=main,Path=Minimum}" Value="{Binding ElementName=main,Path=SliderValue}" /> </Grid> </UserControl>
跟老趙那個類似,實際的使用場景也就是這么回事,cs代碼當時根本就沒想到用MVVM,基本沒有邏輯,沒必要分離。
只是需要綁定,所以DP少不了的。比較簡單,要細看的自己展開。

1 /// <summary> 2 /// when value changed,use eventhandle to notify 3 /// </summary> 4 /// <param name="value"></param> 5 public delegate void CustomSliderEventHandle(double value); 6 7 public partial class UCSlider : UserControl 8 { 9 public UCSlider() 10 { 11 InitializeComponent(); 12 sl.ValueChanged += new RoutedPropertyChangedEventHandler<double>(sl_ValueChanged); 13 } 14 15 #region event 16 public event CustomSliderEventHandle ValueChange; 17 18 void sl_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) 19 { 20 if (ValueChange != null) 21 ValueChange(sl.Value); 22 } 23 #endregion 24 25 #region Text 26 public string Text 27 { 28 get { return (string)GetValue(TextProperty); } 29 set { SetValue(TextProperty, value); } 30 } 31 32 public static DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(UCSlider)); 33 #endregion 34 35 #region Maximum 36 public double Maximum 37 { 38 get { return (double)GetValue(MaximumProperty); } 39 set { SetValue(MaximumProperty, value); } 40 } 41 42 public static DependencyProperty MaximumProperty = DependencyProperty.Register("Maximum", typeof(double), typeof(UCSlider)); 43 #endregion 44 45 #region Minimum 46 public double Minimum 47 { 48 get { return (double)GetValue(MinimumProperty); } 49 set { SetValue(MinimumProperty, value); } 50 } 51 52 public static DependencyProperty MinimumProperty = DependencyProperty.Register("Minimum", typeof(double), typeof(UCSlider)); 53 #endregion 54 55 #region SliderValue 56 public double SliderValue 57 { 58 get { return (double)GetValue(SliderValueProperty); } 59 set { SetValue(SliderValueProperty, value); } 60 } 61 62 public static DependencyProperty SliderValueProperty = DependencyProperty.Register("SliderValue", typeof(double), typeof(UCSlider)); 63 #endregion 64 }
沒有用MVVM,所以比老趙那個簡單了不少。
到了調用的地方,貌似是搞個ViewModel的時候,其實只是用到了NotifyPropertyChanged,因為這個玩起來爽啊。
UI:
<StackPanel HorizontalAlignment="Left" Margin="10,10,0,0" > <UC:UCSlider Text="Brightness" Maximum="100" Minimum="0" SliderValue="{Binding Path=Brightness,Mode=TwoWay}" > </UC:UCSlider> <UC:UCSlider Text="Contrast" Maximum="100" Minimum="0" SliderValue="{Binding Path=Contrast,Mode=TwoWay}"/> <UC:UCSlider Text="Hue" Maximum="2" Minimum="-2" SliderValue="{Binding Path=Hue,Mode=TwoWay}"/> </StackPanel>
CS:

1 /// <summary> 2 /// Interaction logic for MainWindow.xaml 3 /// </summary> 4 public partial class MainWindow : Window 5 { 6 TurningViewModel _turningViewModel = new TurningViewModel() { Hue = 1, Brightness = 20, Contrast = 2 }; 7 public MainWindow() 8 { 9 InitializeComponent(); 10 this.DataContext = _turningViewModel; 11 _turningViewModel.PropertyChanged += new PropertyChangedEventHandler(_turningViewModel_PropertyChanged); 12 } 13 14 void _turningViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e) 15 { 16 Debug.WriteLine(string.Format("PropertyName:{0}", e.PropertyName)); 17 } 18 19 private void get_Click(object sender, RoutedEventArgs e) 20 { 21 Debug.WriteLine(string.Format("Hue:{0}", _turningViewModel.Hue)); 22 Debug.WriteLine(string.Format("Brightness:{0}", _turningViewModel.Brightness)); 23 Debug.WriteLine(string.Format("Contrast:{0}", _turningViewModel.Contrast)); 24 } 25 } 26 27 public class TurningViewModel : INotifyPropertyChanged 28 { 29 #region Hue 30 private double _hue; 31 public double Hue 32 { 33 get { return _hue; } 34 set 35 { 36 if (_hue != value) 37 { 38 _hue = value; 39 NotifyPropertyChanged("Hue"); 40 } 41 } 42 } 43 #endregion 44 45 #region Brightness 46 private double _brightness; 47 public double Brightness 48 { 49 get { return _brightness; } 50 set 51 { 52 if (_brightness != value) 53 { 54 _brightness = value; 55 NotifyPropertyChanged("Brightness"); 56 } 57 } 58 } 59 #endregion 60 61 #region Contrast 62 private double _contrast; 63 public double Contrast 64 { 65 get { return _contrast; } 66 set 67 { 68 if (_contrast != value) 69 { 70 _contrast = value; 71 NotifyPropertyChanged("Contrast"); 72 } 73 } 74 } 75 #endregion 76 77 void NotifyPropertyChanged(string propertyName) 78 { 79 if (PropertyChanged != null) 80 { 81 PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); 82 } 83 } 84 85 public event PropertyChangedEventHandler PropertyChanged; 86 }
基本上就結束了呀。
如果要把這個控件做成自定義控件,繼承自Control的,
XAML:

1 <Style TargetType="{x:Type local:CCSlider}"> 2 <Setter Property="Template"> 3 <Setter.Value> 4 <ControlTemplate TargetType="{x:Type local:CCSlider}"> 5 <Border Background="{TemplateBinding Background}" 6 BorderBrush="{TemplateBinding BorderBrush}" 7 BorderThickness="{TemplateBinding BorderThickness}"> 8 <Grid Width="200" Height="45"> 9 <TextBlock Text="{TemplateBinding Text}" Margin="0,0,0,0" Foreground="Black" VerticalAlignment="Top" HorizontalAlignment="Left"/> 10 <TextBlock Text="{Binding ElementName=sl,Path=Value,StringFormat=0}" Foreground="Black" HorizontalAlignment="Right"/> 11 <Slider Name="sl" Width="{Binding Path=Width,ElementName=main}" VerticalAlignment="Bottom" Cursor="Hand" 12 Maximum="{TemplateBinding Maximum}" 13 Minimum="{TemplateBinding Minimum}" 14 Value="{Binding Path=SliderValue,Mode=TwoWay,RelativeSource={RelativeSource Mode=TemplatedParent}}" /> 15 </Grid> 16 </Border> 17 </ControlTemplate> 18 </Setter.Value> 19 </Setter> 20 </Style>
CS 代碼和 上面那個一樣的。

1 public class CCSlider : Control 2 { 3 static CCSlider() 4 { 5 DefaultStyleKeyProperty.OverrideMetadata(typeof(CCSlider), new FrameworkPropertyMetadata(typeof(CCSlider))); 6 } 7 8 9 #region Text 10 public string Text 11 { 12 get { return (string)GetValue(TextProperty); } 13 set { SetValue(TextProperty, value); } 14 } 15 16 public static DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(CCSlider)); 17 #endregion 18 19 #region Maximum 20 public double Maximum 21 { 22 get { return (double)GetValue(MaximumProperty); } 23 set { SetValue(MaximumProperty, value); } 24 } 25 26 public static DependencyProperty MaximumProperty = DependencyProperty.Register("Maximum", typeof(double), typeof(CCSlider)); 27 #endregion 28 29 #region Minimum 30 public double Minimum 31 { 32 get { return (double)GetValue(MinimumProperty); } 33 set { SetValue(MinimumProperty, value); } 34 } 35 36 public static DependencyProperty MinimumProperty = DependencyProperty.Register("Minimum", typeof(double), typeof(CCSlider)); 37 #endregion 38 39 #region SliderValue 40 public double SliderValue 41 { 42 get { return (double)GetValue(SliderValueProperty); } 43 set { SetValue(SliderValueProperty, value); } 44 } 45 46 public static DependencyProperty SliderValueProperty = DependencyProperty.Register("SliderValue", typeof(double), typeof(CCSlider)); 47 #endregion 48 }
順便嘮叨:
1. 控件自定義有兩種:
一種是和邏輯相關的,里面可以搞搞ViewModel,分離一下。如果分離,DataContext必須指明,要不然和老趙的那個錯誤類似了。如果邏輯不復雜,就沒那個必要了,直接balala一搞就完事兒了。
如果業務粒度分的細,邏輯比較單純的,還是不要費那個事兒了,畢竟ViewModel的代碼量大啊,我承認自己懶。
這個ASP.net的模板頁面套子頁面一個道理。
這一種用UserControl搞搞,方便,感覺就是為了分割邏輯用的。
另外一種是和邏輯無關的,例如三態按鈕,要大量復用,或者要做類似RadioButton的效果,只有一個被選中。
那就繼承自Control吧。
這個區分方法也不是嚴格定義的,看實際需求吧。
2.再來說MVVM,關於這個東東的爭論貌似也不少,快趕上 JAVA和C#的爭論了。
不少的WPF開發者,潛意識里面,隨便寫個東東都MVVM,不管大大小小都搞個ViewModel。
有時候.xaml.cs里面幾乎啥都沒放,ViewModel的代碼倒是急劇膨脹,何苦啊。
MVVM的理論解釋,還是有微軟的說法,等等,都在MSDN里面,不再貼出來了。
只說說自己的體會:
剛開始搞WPF的時候,從ASP.net轉過來的,WinForm也玩過,習慣了代碼搞定一切。
於是乎,套用,幾乎一切都是代碼搞定,因為這個相對熟悉,而且調試方便啊。
后來項目湊合着搞完了,開始靜下心來看看WPF,看看MVVM,看看綁定啊等等東西。|
也做了Demo,發現如果熟悉XAML,挺好,一句可以代替CS代碼好多句,清爽,簡練,不熟悉,苦逼的事情很多。
MVVM,第一次用是在 數據的增刪改,新增家具,要填寫一堆的屬性,家具列表,修改家具...
按照之前的做法,寫個類,一個一個屬性的綁定,保存的時候,一個一個的獲取...,你懂的,十幾個屬性,會死人的。
用MVVM,只要在XAML里面綁定數據,UI邏輯需要訪問控件的,在.xaml.cs中寫一下,數據加工邏輯不會涉及控件的都在ViewModel中,獲取數據的時候直接取ViewModel。
這是我發現的最最適用的場合。
我想到最初做Winform的時候,有個叫DataGrid(名字不太確定)的控件,直接可以編輯表格內容,排序,增刪改都集成了。
當時是醫葯銷售行業,老大們把業務邏輯封裝在存儲過程中,UI就直接拿DataGrid,拖過來,給個數據源,保存的時候取一下數據,完事兒了。
並且賣錢了,還不少,這就是MS最牛叉的地方,直接用,直接賣錢。
數據增刪改,是直接產生效益的地方,一般的銷售類軟件都需要這個功能,微軟貌似就在這里下功夫,Asp.net 和Winfor都是,WPF的MVVM也是。
再回頭說說做的第一個項目,后來准備重構,弄個MVVM上去,發現不爽。
這個項目類似塗鴉牆,文字、圖片、Flash等等可以隨意的放在畫布上,而且可以移動、旋轉、縮放。
原因是數據加工邏輯基本沒有,就是保存,提取。邏輯集中在UI上,最要命的是移動、旋轉、縮放,而且還組合操作。
如果我用MVVM,倒也可以,但是還是用CS代碼控制比較爽。
說說現在的看法:
舉個例子,ListView,數據加載\加工的邏輯在ViewModel,雙擊列頭排序和雙擊條目跳轉這種邏輯在.xaml.cs中。
再舉個例子,帶有復選框文件樹,用TreeView實現的,勾選子結點,父節點被勾選還是咋的,純粹是數據層面的事情,那就在ViewModel中實現。
某個節點被選中了,要獲取當前的選中項數據,在Xaml.cs中顯然方便一點,在ViewModel中檢測PropertyChange當然也可以。
數據相關的在ViewModel中,需要訪問控件的在.xaml.cs中,當然了,有時候也可以做一下均衡。
只有后者的,不實現MVVM。
再扯遠點,就是數據驅動開發了。
其實就一句話:根據實際業務情況決定和重構。