准備.Net轉前端開發-WPF界面框架那些事,值得珍藏的8個問題


題外話

    不出意外,本片內容應該是最后一篇關於.Net技術的博客,做.Net的伙伴們忽噴忽噴。.Net挺好的,微軟最近在跨平台方面搞的水深火熱,更新也比較頻繁,而且博客園的很多大牛也寫的有跨平台相關技術的博客。做.Net開發塊五年時間,個人沒本事,沒做出啥成績。想象偶像梅球王,年齡都差不多,為啥差別就這么大。不甘平庸,想趁機會挑戰下其他方面的技術,正好有一個機會轉前段開發。

    對於目前正在從事或者工作中會用到WPF技術開發的伙伴,此片內容不得不收藏,本片介紹的八個問題都是在WPF開發工作中經常使用到並且很容易搞錯的技術點。能輕車熟路的掌握這些問題,那么你的開發效率肯定不會低。

WPF相關鏈接

No.1 准備.Net轉前端開發-WPF界面框架那些事,搭建基礎框架

No.2 准備.Net轉前端開發-WPF界面框架那些事,UI快速實現法

No.3 准備.Net轉前端開發-WPF界面框架那些事,值得珍藏的8個問題

8個問題歸納

No.1.WrapPane和ListBox強強配合;

No.2.給數據綁定轉換器Converter傳多個參數;

No.3.搞清楚路由事件的兩種策略:隧道策略和冒泡策略;

No.4.Conveter比你想象的強大;

No.5.ItemsControl下操作指令的綁定;

No.6.StaticResource和DynamicResource區別;

No.7.數據的幾種綁定形式;

No.8.附加屬性和依賴屬性;

八大經典問題

1. WrapPane和ListBox強強配合

    重點:WrapPanel在ListBox面板中的實現方式

    看一張需求圖片,圖片中需要實現的功能是:左邊面板顯示內容,右邊是一個圖片列表,由於圖片比較多,所以在左向左拖動中間分隔線時,圖片根據右欄的寬度的增加,每行可顯示多張圖片。功能是很簡單,但有些人寫出這個功能需要2個小時,而有些人只需要十幾分鍾

image

    循序漸進,我們先實現每行顯示一張圖片,直接使用StackPanel重寫ItemPanel模板,並設置Orientation為Vertical。實現結果如下:

image

   源代碼如下:

<Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="2" />
            <ColumnDefinition Width="120" />
        </Grid.ColumnDefinitions>
        <Border BorderBrush="Green" Grid.Column="0">
            <Image Source="{Binding ElementName=LBoxImages, Path=SelectedItem.Source}" />
        </Border>
        <GridSplitter Grid.Column="1" VerticalAlignment="Stretch" HorizontalAlignment="Center" BorderThickness="1" BorderBrush="Green" />
        <ListBox Name="LBoxImages" Grid.Column="2">
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <StackPanel Orientation="Vertical" />
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
            <Image Source="/Images/g1.jpg" Width="100" Height="80" />
            <Image Source="/Images/g2.jpg" Width="100" Height="80" />
            <Image Source="/Images/g3.jpg" Width="100" Height="80" />
            <Image Source="/Images/g4.jpg" Width="100" Height="80" />
            <Image Source="/Images/g5.jpg" Width="100" Height="80" />
            <Image Source="/Images/g7.jpg" Width="100" Height="80" />
            <Image Source="/Images/g8.jpg" Width="100" Height="80" />
        </ListBox>
    </Grid>

    分析執行結果圖,游覽顯示的圖片列表,不管ListBox有多寬,總是只顯示一行,現在我們考慮實現每行根據ListBox寬度自動顯示多張圖片。首先,StackPanel是不支持這樣的顯示,而WrapPanel可動態排列每行。所以需要把StackPanel替換為WrapPanel並按行優先排列。修改代碼ItemsPanelTemplate代碼:

<ItemsPanelTemplate>
            <WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>

    再看看顯示結果:

image

    和我們的預期效果不一樣,為什么會這樣?正是這個問題讓很多人花幾個小時都沒解決。第一個問題:如果要實現自動換行我們得限制WrapPanel的寬度,所以必須顯示的設置寬度,這個時候很多人都考慮的是把WrapPanel的寬度和ListBox的寬度一致。代碼如下:

<ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <WrapPanel Orientation="Horizontal" Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBox}}, Path=Width}" />
                </ItemsPanelTemplate>
</ListBox.ItemsPanel>

    需要強調的是,這樣修改后結果還是一樣的。為什么會這樣?第二個問題:里出現的問題和CSS和HTML相似,ListBox有邊框,所以實際顯示內容的Width小於ListBox的Width。而你直接設置WrapPanel的寬度等於ListBox的寬度,肯定會顯示不完,所以滾動條還是存在。因此,我們必須讓WrapPanel的Width小於ListBox的Width。我們可以寫個Conveter,讓WrapPanel的Width等於ListBox的Width減去10個像素。Converter代碼如下:

public class SubConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if(value == null)
            {
                throw new ArgumentNullException("value");
            }
            int listBoxWidth;
            if(!int.TryParse(value.ToString(), out listBoxWidth))
            {
                throw new Exception("invalid value!");
            }
            int subValue = 0;
            if(parameter != null)
            {
                int.TryParse(parameter.ToString(), out subValue);
            }
            return listBoxWidth - subValue;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

    為WrapPanel的Width綁定添加Converter,代碼如下:

<ItemsPanelTemplate>
                    <WrapPanel Orientation="Horizontal" Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBox}}, Path=ActualWidth, Converter={StaticResource SubConverter}, ConverterParameter=10}" />
                </ItemsPanelTemplate>

     這下就大功告成,看看結果效果,是不是每行自動顯示多張圖片了?

    image

2.給數據綁定轉換器Converter傳多個參數

    重點:MultiBinding和IMultiValueConverter。

    看看下面功能圖片:

image

    上圖中,有報警、提示、總計三個統計數。總計數是報警和提示數量的總和,如果報警和提示數發生變化,總計數自動更新。其實這個功能也非常簡單,很多人實現的方式是定義一個類,包含三個屬性。代碼如下:

public class Counter : INotifyPropertyChanged
    {
        private int _alarmCount;
        public int AlarmCount
        {
            get { return _alarmCount; }
            set
            {
                if(_alarmCount != value)
                {
                    _alarmCount = value;
                    OnPropertyChanged("AlarmCount");
                    OnPropertyChanged("TotalCount");
                }
            }
        }

        private int _messageCount;
        public int MessageCount
        {
            get { return _messageCount; }
            set
            {
                if(_messageCount != value)
                {
                    _messageCount = value;
                    OnPropertyChanged("MessageCount");
                    OnPropertyChanged("TotalCount");
                }
            }
        }

        public int TotalCount
        {
            get { return AlarmCount + MessageCount; }
        }
    }

    這里明顯有個問題時,如果統計的數量比較多,那么TotalCount需要加上多個數,並且每個數據屬性都得添加OnPropertyChanged("TotalCount")觸發界面更新TotalCount數據。接下來我們就考慮考慮用Converter去實現該功能,很多人都知道IValueConverter,但有些還沒怎么使用過IMultiValueConverter接口。IMultiValueConverter可接收多個參數。通過IMultiValueConverter,可以讓我們不添加任何后台代碼以及耦合的屬性。IMultiValueConverter實現代碼如下:

public class AdditionConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            if(values == null || values.Length != 2)
            {
                return 0;
            }
            int alarmCount = 0;
            if(values[0] != null)
            {
                int.TryParse(values[0].ToString(), out alarmCount);
            }
            int infoCount = 0;
            if(values[1] != null)
            {
                int.TryParse(values[1].ToString(), out infoCount);
            }

            return (alarmCount + infoCount).ToString();
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

    接下來就是通過MultiBinding實現多綁定。代碼如下:

<Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <StackPanel Margin="10" Grid.Column="0" Orientation="Horizontal" Background="Red">
            <Label VerticalAlignment="Center" Content="報警:" />
            <TextBox Name="TBoxAlarm" Background="Transparent" Height="30" Width="60" VerticalContentAlignment="Center" VerticalAlignment="Center" />
        </StackPanel>
        <StackPanel Margin="10" Grid.Column="1" Orientation="Horizontal" Background="Green">
            <Label VerticalAlignment="Center" Content="提示:" />
            <TextBox Name="TBoxInfo" Background="Transparent" Height="30" Width="60" VerticalContentAlignment="Center" VerticalAlignment="Center" />
        </StackPanel>
        <StackPanel Margin="10" Grid.Column="2" Orientation="Horizontal" Background="Gray">
            <Label VerticalAlignment="Center" Content="總計:" />
            <TextBox Name="TBoxTotal" Background="Transparent" Height="30" Width="60" VerticalContentAlignment="Center" VerticalAlignment="Center">
                <TextBox.Text>
                    <MultiBinding Converter="{StaticResource AdditionConverter}">
                        <Binding ElementName="TBoxAlarm" Path="Text" />
                        <Binding ElementName="TBoxInfo" Path="Text" />
                    </MultiBinding>
                </TextBox.Text>
            </TextBox>
        </StackPanel>
    </Grid>

   這里,我們沒有修改任何后台代碼以及添加任何Model屬性。這里這是舉個簡單的例子,說明MultiBinding和IMultiValueConverter怎樣使用。

3.搞清楚路由事件的兩種策略:隧道策略和冒泡策略

    WPF中元素的事件響由事件路由控制,事件的路由分兩種策略:隧道和冒泡策略。要解釋着兩種策略,太多文字都是廢話,直接上圖分析:

image

    上圖中包含三個Grid,它們的邏輯樹層次關系由上到下依次為Grid1->Grid1_1->Grid1_1_1。例如我們鼠標左鍵單擊Grid1_1_1,隧道策略規定事件的傳遞方向由樹的最上層往下傳遞,在上圖中傳遞的順序為Grid1->Grid1_1->Grid1_1_1。而路由策略規定事件的傳遞方向由邏輯樹的最下層往上傳遞,就像冒泡一樣,從最下面一層一層往上冒泡,傳遞的順序為Grid1_1_1->Grid1_1->Grid1。如果體現在代碼上,隧道策略和冒泡策略有特征?

    (1)元素的事件一般都是隧道和冒泡成對存在,隧道事件包含前綴Preiview,而冒泡事件不包含Preview。就拿鼠標左鍵單擊事件舉例,隧道事件為PreviewMouseLeftButtonDown,而冒泡事件為MouseLeftButtonDown。

    (1)隧道策略事件先被觸發,隧道策略觸發完后才觸發冒泡策略事件。例如單擊Grid1_1_1,整個路由事件的觸發順序為:Grid1(隧道)->Grid1_1(隧道)->Grid1_1_1(隧道)->Grid1_1_1(冒泡)->Grid1_1(冒泡)->Grid1(冒泡)。

    (3)路由事件的EventArgs為RoutedEventArgs,包含Handle屬性。如果在觸發順序的某個事件上設置了Handle等於true,那么它之后的隧道事件和冒泡事件都不會觸發了。

    接下來我們就寫例子分析,先看看界面的代碼:

<Grid Width="400" Height="400" Background="Green" Name="Grid1" PreviewMouseLeftButtonDown="Grid1_PreviewMouseLeftButtonDown" MouseLeftButtonDown="Grid1_MouseLeftButtonDown">
        <Grid Width="200" Height="200" Background="Red" Name="Grid1_1" PreviewMouseLeftButtonDown="Grid1_1_PreviewMouseLeftButtonDown" MouseLeftButtonDown="Grid1_1_MouseLeftButtonDown">
            <Grid Width="100" Height="100" Background="Yellow" Name="Grid1_1_1" PreviewMouseLeftButtonDown="Grid1_1_1_PreviewMouseLeftButtonDown" MouseLeftButtonDown="Grid1_1_1_MouseLeftButtonDown">
            </Grid>
        </Grid>
    </Grid>

    我們為每個Grid都配置了隧道事件PreviewMouseLeftButtonDown,冒泡事件MouseLeftButtonDown。然后分別實現事件內容:

public partial class RoutedEventWindow : Window
    {
        public RoutedEventWindow()
        {
            InitializeComponent();
        }
        #region 隧道策略
        private void Grid1_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            PrintEventLog(sender, e, "隧道");
            e.Handled = true;
        }

        private void Grid1_1_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            PrintEventLog(sender, e, "隧道");
        }

        private void Grid1_1_1_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            PrintEventLog(sender, e, "隧道");
        }

        #endregion

        private void PrintEventLog(object sender, RoutedEventArgs e, string action)
        {
            var host = sender as FrameworkElement;
            var source = e.Source as FrameworkElement;
            var orignalSource = e.OriginalSource as FrameworkElement;
            Console.WriteLine("策略:{3}, sender: {2}, Source: {0}, OriginalSource: {1}", source.Name, orignalSource.Name, host.Name, action);
        }

        #region 冒泡策略

        private void Grid1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            PrintEventLog(sender, e, "冒泡策略");
        }

        private void Grid1_1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            PrintEventLog(sender, e, "冒泡策略");
        }

        private void Grid1_1_1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            PrintEventLog(sender, e, "冒泡策略");
        }

        #endregion
    }

    事件的代碼很簡單,都調用了PrintEventLog方法,打印每個事件當前觸發元素(sender)以及觸發源(e.Source)和原始源(e.OriganlSource)。現在我們就把鼠標移到Grid1_1_1上面(黃色),單擊鼠標鼠標左鍵。打印的日志結果為:

策略:隧道, sender: Grid1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
策略:隧道, sender: Grid1_1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
策略:隧道, sender: Grid1_1_1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
策略:冒泡策略, sender: Grid1_1_1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
策略:冒泡策略, sender: Grid1_1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
策略:冒泡策略, sender: Grid1, Source: Grid1_1_1, OriginalSource: Grid1_1_1

    分析打印的日志是不是和我們上面描述的特征一樣,先觸發隧道事件(Grid1->Grid1_1->Grid1_1_1),然后再觸發冒泡事件(Grid1_1_1->Grid1_1->Grid1),並且和我們描述的事件傳遞方向也一致?如果還不信我們可以跟進一步測試,我們知道最后一個被觸發的隧道事件是Grid1_1_1上的Grid1_1_1_PreviewMouseLeftButtonDown,如果我在該事件上設置e.Handle等於true,按理來說,后面的3個冒泡事件都不會觸發。代碼如下:

private void Grid1_1_1_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            PrintEventLog(sender, e, "隧道");
            e.Handled = true;
        }

   輸出結果如下:

策略:隧道, sender: Grid1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
策略:隧道, sender: Grid1_1, Source: Grid1_1_1, OriginalSource: Grid1_1_1
策略:隧道, sender: Grid1_1_1, Source: Grid1_1_1, OriginalSource: Grid1_1_1

    結果和我們之前描的推理一致,后面的3個冒泡事件確實沒有觸發。弄清楚路由事件的機制非常重要,因為在很多時候我們會遇到莫名其妙的問題,例如我們為ListBoxItem添加MouseRightButtonDown事件,但始終沒有被觸發。究其原因,正是因為ListBox自己處理了該事件,設置Handle等於true。那么,ListBox包含的元素的MouseRightButtonDown肯定不會被觸發。

4.Conveter比你想象的強大

    什么時候會用到Converter?做WPF的開發人員經常會遇到Enable狀態轉換Visibility狀態、字符串轉Enable狀態、枚舉轉換字符串狀態等。我們一般都知道IValueConverter接口,實現該接口可以很容易的處理上面這些情況。下面是Enable轉Visibility代碼:

public class EnableToVisibilityConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if(value == null || !(value is bool))
            {
                throw new ArgumentNullException("value");
            }
            var result = (bool)value;

            return result ? Visibility.Visible : Visibility.Collapsed;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

    另外一種情況,例如我們在列表中顯示某些數據,如果我們想給這些數據加上單位,方法有很多,但用converter實現的應該比較少見。這種情況我們可以充分利用IValueConverter的Convert方法的parameter參數。實現代碼如下:

/// <param name="value">數字</param>
 /// <param name="targetType"></param>
/// <param name="parameter">單位</param>
/// <param name="culture"></param>
 /// <returns></returns>
  public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
 {
            if(value == null)
            {
                return string.Empty;
            }
            if(parameter == null)
            {
                return value;
            }
            return value.ToString() + " " + parameter.ToString();
}

    界面代碼如下:

<TextBlock Text="{Binding Digit, Converter={StaticResource UnitConverter}, ConverterParameter='Kg'}"></TextBlock>

    稍微復雜的情況,如果需要根據2個甚至多個值轉換為一個結果。例如我們需要根據一個人的身高、存款、顏值,輸出高富帥、一般、矮窮矬3個狀態。這種情況IValueConverter已不再滿足我們的要求,但Converter給我們提供了IMultiValueConverter接口。該接口可同時接收多個參數。按照前面的需求實現Converter,代碼如下:

public class PersonalStatusConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            //values:身高、顏值、存款
            if (values == null || values.Length != 3)
            {
                throw new ArgumentException("values");
            }
            int height; //身高
            int faceScore; //顏值
            decimal savings; //存款

            try
            {
                if(int.TryParse(values[0].ToString(), out height) && 
                   int.TryParse(values[1].ToString(), out faceScore) &&
                   decimal.TryParse(values[2].ToString(), out savings))
                {
                    //高富帥條件:身高180 CM以上、顏值9分以上、存款1000萬以上
                    if(height >=180 && faceScore >=9 && savings >= 10000 * 1000)
                    {
                        return "高富帥";
                    }
                    //矮窮矬條件:身高不高於10CM,顏值小於1,存款不多於1元
                    if(height <= 10 && faceScore <= 1 && savings <= 1)
                    {
                        return "矮窮矬";
                    }
                }
            }
            catch (Exception ex){}

            return "身份未知";
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

    實現了IMultiValueConverter接口后,我們還得知道怎樣使用。這里我們還得配合MultiBinding來實現綁定多個參數。下面就根據一個測試例子看看如何使用它,代碼如下:

<Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="5" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <StackPanel VerticalAlignment="Center" Orientation="Horizontal" Grid.Row="0">
            <Label Content="身高(CM):" />
            <TextBox  Name="TBoxHeight"  Width="60"/>
            <Label Content="顏值(0-10):" />
            <TextBox Name="TBoxScore" Width="60" />
            <Label Content="存款(元):" />
            <TextBox Name="TBoxSaving" Width="100" />
        </StackPanel>
        <StackPanel Grid.Row="2" Orientation="Horizontal" VerticalAlignment="Center">
            <Label Content="測試結果:" />
            <TextBox IsReadOnly="True" Width="150">
                <TextBox.Text>
                    <MultiBinding Converter="{StaticResource PersonalStatusConverter}">
                        <Binding ElementName="TBoxHeight" Path="Text" />
                        <Binding ElementName="TBoxScore" Path="Text" />
                        <Binding ElementName="TBoxSaving" Path="Text" />
                    </MultiBinding>
                </TextBox.Text>
            </TextBox>
        </StackPanel>
    </Grid>

    通過代碼可以看出MultiBinding和IMultiValueConverter一般都是同時使用的

    測試界面如下:

image

    通過上面的介紹,我們應該知道怎樣使用IValueConverter和IMultiValueConverter接口了。

5.ItemsControl下操作指令的綁定

    先看看下面的列表,展示人的頭像、個人信息,並且提供刪除功能。如下所示:

 image

    這里最想說的是關於Delete操作的指令綁定,這一點很容易出問題。有些時候我們綁定了指令,但是單擊Delete按鈕沒有任何反應。這里涉及到兩個數據源,一個是列表集合數據源,一個是指令上下文數據源。例如上面的列表存放在UserControl里邊,一般ListItem的數據源來源於List的ItemsSource,而按鈕的Command來源於UserControl數據源。下面是實現的界面代碼:

<Window x:Class="HeaviSoft.Wpf.ErrorDemo.PortraitWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:HeaviSoft.Wpf.ErrorDemo"
        xmlns:converter="clr-namespace:HeaviSoft.Wpf.ErrorDemo.Conveters"
        mc:Ignorable="d"
        Title="是不是美女" Height="400" Width="600">
    <Window.Resources>
        <converter:SubConverter x:Key="SubConverter"></converter:SubConverter>
        <Style TargetType="{x:Type Button}">
            <Setter Property="Background" Value="Transparent"/>
        </Style>
        <Style TargetType="{x:Type TextBlock}">
            <Setter Property="FontFamily" Value="KaiTi" />
            <Setter Property="Foreground" Value="Honeydew" />
        </Style>
    </Window.Resources>
    <ListBox ItemsSource="{Binding PortaitList}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Border Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBox}}, Converter={StaticResource SubConverter}, ConverterParameter=10, Path=ActualWidth}" BorderThickness="0, 0, 0, 1" BorderBrush="Gray">
                    <DockPanel  VerticalAlignment="Center" LastChildFill="False">
                        <Image Margin="5, 0" DockPanel.Dock="Left" Source="{Binding Photo}" Width="40" Height="40" />
                        <TextBlock DockPanel.Dock="Left" Text="{Binding Name}" VerticalAlignment="Center" />
                        <TextBlock DockPanel.Dock="Left" Text=", " VerticalAlignment="Center" />
                        <TextBlock DockPanel.Dock="Left" Text="{Binding Birthday}" VerticalAlignment="Center" />
                        <TextBlock DockPanel.Dock="Left" Text=", "  VerticalAlignment="Center" />
                        <TextBlock DockPanel.Dock="Left" Text="{Binding Vocation}" VerticalAlignment="Center" />
                        <Button Margin="0,3, 5, 3" Padding="4, 2" 
                                Command="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:PortraitWindow}}, Path=DataContext.DeleteCommand}" CommandParameter="{Binding .}" DockPanel.Dock="Right" Content="Delete" />
                    </DockPanel>
                </Border>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Window>

   數據源代碼:

public class PortaitViewModel : ModelBase
    {
        public PortaitViewModel(IEnumerable<Portait> portaits)
        {
            PortaitList = new ObservableCollection<Portait>(portaits);
        }

        public ObservableCollection<Portait> PortaitList { get; set; }

        private ICommand _deleteCommand;
        public ICommand DeleteCommand
        {
            get
            {
                return _deleteCommand ?? new UICommand()
                {
                    Executing = (parameter) =>
                    {
                        PortaitList.Remove(parameter as Portait);
                    }
                };
            }
        }
    }

    首先,PortraitWindow綁定數據源PortaitViewModel,ListBox綁定了一個集合屬性PortaitList,所以ListItem對應集合中的一項,也就是一個Portrait對象。我們重點要看的是Button如何綁定Command。這里再單獨把Button代碼貼出來:

<Button Margin="0,3, 5, 3" Padding="4, 2" 
                                Command="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:PortraitWindow}}, Path=DataContext.DeleteCommand}" CommandParameter="{Binding .}" DockPanel.Dock="Right" Content="Delete" />

    Button是在ListItem中,我們剛才說ListItem的數據源為一個Portrait對象,所以我們CommandParameter可直接綁定對象{Binding .},而Command需要切換到PortraitWindow的數據上下文中,這里我們使用了RelativeSource,向上遍歷查找邏輯樹PortraitWindow,並綁定它的數據DataContext.DeleteCommand。需要要強調的是x:Type要寫PortraitWindow而不是Window,否則有時候會出問題的。

    界面執行結果如下:

image

6.StaticResource和DynamicResource區別

    先不忙描述這兩者的區別,看看應用場景。一般的系統都支持多主題和多語言,我們知道主題和語言都是資源。既然都是資源,那么就涉及到如何引用資源,用StaticResource還是DynamicResource?主題和語言資源的引用必須滿意一個條件,就是我在系統運行過程中,切換主題或語言之后,我們的界面資源馬上也被切換,也就是隨時支持更新。帶着這樣一個場景,再看看兩者的區別:

    (1)StaticResource只被加載一次,DynamicResource每次改變都可重新引用。

    (2)一般使用DynamicResource的地方都可以使用StaticResource代替。因為DynamicResource只能用着設置依賴屬性的值,而StaticResource可被用到任何地方。例如下面的這種情況就是可使用StaticResource但不能使用DynamicResource:

<Window xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
    xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”>
    <Window.Resources>
        <Image x:Key=”zoom” Height=”21” Source=”zoom.gif”/>
    </Window.Resources>
    <StackPanel>
        <StaticResource ResourceKey=”zoom”/>
    </StackPanel>
</Window>

    (3)DynamicResource比StaticResource占用更多的開銷,因為它需要額外的跟蹤,跟蹤哪些地方引用了Dynamic資源。如果資源更新了,引用它的地方也會被更新到。

    (4)很多人可能覺得DynamicResource加載時間比StaticResource長,但恰恰相反。因為在加載界面時,所有StaticResource引用都會馬上加載出來;而DynamicResource只會在真正使用時才會加載。

7.數據的幾種綁定形式

    Binding這個類有時候真把開發人員搞糊塗,特別是它的幾個屬性,像Mode、UpdateSourceTrigger、NotifyOnSourceUpdated、NotifyOnTargetUpdated。所以,想要熟練使用Binding,這幾個屬性不得不掌握。

    (1).Mode屬性

    是一個枚舉值,枚舉項包括Default、TwoWay(雙向綁定)、OneWay、OneWayToSource、OneTime。Default是一個強大的枚舉項,為什么這么說?如果我們跟蹤代碼,控件的Binding的Mode屬性一般為Default。Default其實就是后面幾個枚舉中的一種情況,具體是哪一種要根據控件的選擇。例如一個TextBox,那么Default為TwoWay模式。如果是一個TextBlock,那么Default為OneWay模式。所以一般情況下,不建議設置Mode。下圖是Mode示意圖,通過圖片可以很清楚的了解這幾個模式:

image

    (2).UpdateSourceTrigger

    更新數據源觸發器,分析Mode的示意圖,可看出只有在TwoWay和OneWayToSource情況下才會更新Source。UpdateSourceTrigger也是一個枚舉,枚舉項包括:Default、Explicit、LostFocus、PropertyChanged。和Mode相似,控件默認綁定的UpdateSourceTrigger一般為Default,也是根據不同的情況選擇后面的幾個枚舉值。實際情況,一般觸發器都是為LostFocus,所以有些時候我們需要值發生變化馬上更新數據源,那么必須設置UpdateSourceTrigger=”PropertyChanged”。

    (3)NotifyOnSourceUpdated和NotifyOnTargetUpdated

    其實這兩個屬性一般都不用配置,NotifyOnSourceUpdated表示當通過控件更新了數據源后是否觸發SourceUpdated事件。NotifyOnTargetUpdated恰好相反,表示當通過數據源更新了控件數據后,是否觸發TargetUpdated事件。既然這兩個屬性控制是否觸發事件,那么這些事件從哪來的?這里要提到Binding的SourceUpdatedEvent和TargetUpdatedEvent事件。下面一段代碼就是連個事件的添加方式:

Binding.AddSourceUpdatedHandler(TBoxResult, new EventHandler<DataTransferEventArgs>((sender1 ,e1) => { /*更新數據源被觸發*/ }));
Binding.AddTargetUpdatedHandler(TBoxResult, new EventHandler<DataTransferEventArgs>((sender1, e1) => { /*更新控件數據被觸發*/}));

8.附加屬性和依賴屬性

    我們先看看兩者的實現代碼由什么區別,依賴屬性和附加屬性實現代碼如下:

#region 依賴屬性
        public static readonly DependencyProperty MyWidthDependencyProperty = DependencyProperty.Register("MyWidth", typeof(int), typeof(CustomControl), new FrameworkPropertyMetadata(
         (d, e) =>
         {
             if (d is CustomControl)
             {
             }
         }));

        public int MyWidth
        {
            get { return (int)GetValue(MyWidthDependencyProperty); }
            set { SetValue(MyWidthDependencyProperty, value); }
        }

        #endregion

        #region 附加屬性
        public static readonly DependencyProperty MyHeightDependencyProperty = DependencyProperty.RegisterAttached("MyHeight", typeof(int), typeof(CustomControl), new FrameworkPropertyMetadata(
         (d, e) =>
         {
             if (d is CustomControl)
             {
             }
         }));

        public static void SetMyHeight(DependencyObject obj, int value)
        {
            obj.SetValue(MyHeightDependencyProperty, value);
        }

        public static int GetMyHeight(DependencyObject obj)
        {
            return (int)obj.GetValue(MyHeightDependencyProperty);
        }
        #endregion

    依賴屬性和附加屬性的聲明方式比較相似,不同的地方一方面依賴屬性調用Register方法,而附加屬性調用RegisterAttached方法。另一方面,依賴屬性一般需要定義一個CLR屬性來使用,而依賴屬性需要定義靜態的Get和Set方法使用。接下來再看看怎樣使用依賴屬性和附加屬性,代碼如下:

<local:CustomControl Grid.Row="1" x:Name="CControl"
                Width="{Binding ElementName=CControl, Path=MyWidth}" 
                Height="{Binding ElementName=CControl, Path=MyHeight}"
                MyHeight="{Binding ElementName=TBoxHeight, Path=Text}" 
                local:CustomControl.MyWidth="{Binding ElementName=TBoxWidth, Path=Text}" 
                Background="Green" Margin="10" />

    在界面上我們可以直接像使用一般屬性一樣使用依賴屬性:MyHeight=””,而使用附加屬性一般都是類名.附加屬性:local:CustomControl.MyWidth=”“。附加屬性我們見到得也比較多,例如Grid.Row、Grid.RowSpan、DockPanel.Dock等。通過上面的分析,我們總結出兩者的區別:

    (1)兩者很相似,都需要定義XXXProperty的靜態只讀屬性;

    (2)依賴屬性使用Register方法注冊,而附加屬性使用RegisterAttached注冊;

    (3)依賴屬性一般需要定義一個CLR屬性來使用,而附加屬性需要定義Set和Get兩個靜態方法;

    (4)依賴屬性定義后,附加的類一直擁有這個依賴屬性。而附加屬性只是需要的時候才附加上去,可有可無;

    (5)依賴屬性和附加屬性都可用於擴展類的屬性。但附加屬性可用於界面布局,像Grid的Grid.Row和DockPanel的Dock屬性。

源代碼

    完整的代碼存放在GitHub上,代碼路徑:https://github.com/heavis/WpfDemo

如果本篇內容對大家有幫助,請點擊頁面右下角的關注。如果覺得不好,也歡迎拍磚。你們的評價就是博主的動力!下篇內容,敬請期待!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM