可復用的WPF或者Silverlight應用程序和組件設計(5)——布局重用


內容概述

我去年寫過幾篇關於在WPF和Silverlight中實現可復用的設計的文章,分別如下,如果有興趣地可以先參考一下

今天要繼續寫第五篇的原因在於最近的一些思考,也是我被問到的一個問題:我們知道WPF中的布局控件有很多,例如Grid,StackPanel,Canvas等等,利用他們,編寫一定的XAML定義,就可以設計出來足夠靈活多樣的界面。但這里會有一個問題,如果我們很多界面都很類似,例如有某種固定格式的布局要求,那么是否在每個界面上都應該去定義一次呢?

[備注]和前幾篇文章不同的是,這一篇沒有錄制視頻,因為我還是覺得文字的部分很重要。

本文源代碼可以通過 http://files.cnblogs.com/chenxizhang/WpfApplicationSample.zip  下載

 

問題詳細描述

想象一下下面這樣一個示意圖

image

如果只有一個界面的話,那么我們可以很簡單地實現,如下面所示

<Window x:Class="WpfApplicationSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:extension="clr-namespace:System.Windows.Extensions;assembly=WPFExtension"
        Title="MainWindow"
        Height="350"
        Width="525">
    
    <Window.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="FontSize"
                    Value="30"></Setter>
        </Style>
    </Window.Resources>
    
    <Grid extension:GridHelper.ShowBorder="True">
        <Grid.RowDefinitions>
            <RowDefinition Height="80"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"></ColumnDefinition>
            <ColumnDefinition Width="100"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        
        
        <!--頂部控件-->
        <ContentControl>
            <ContentControl.Content>
                <TextBlock Text="Top"></TextBlock>
            </ContentControl.Content>
        </ContentControl>
        
        
        <!--主體控件-->
        <ContentControl Grid.Row="1">
            <ContentControl.Content>
                <TextBlock Text="Main"></TextBlock>
            </ContentControl.Content>
        </ContentControl>
        
        <!--右側控件-->
        <ContentControl Grid.Column="1"
                        Grid.RowSpan="2">
            <ContentControl.Content>
                <TextBlock Text="Right"></TextBlock>
            </ContentControl.Content>
        </ContentControl>
    </Grid>
</Window>

這個界面看起來是這樣的:

image

所以,我們完全可以做得出來。但是問題的關鍵在於,如果有很多窗口都是這樣的布局,那么我們是否應該每個窗口都去這樣定義?還是說能否從一定意義上實現布局重用:能不能讓這個Grid默認就有兩行和兩列呢?

[備注] 上面的代碼中,為了給Grid添加邊框線,使用到了我以前寫的一個擴展組件,可以通過這里了解如何使用:http://nuget.org/packages/WPFSilverlightExtension/

 

Windows Forms里面的做法

很久以前,那時候還沒有WPF,我們都使用Windows Forms這個技術來做界面。那時候,對於上面提到的這種布局重用的問題,有一個很簡單的解決方案:窗體繼承。

對於這一種技術,本文並不打算對此進行展開討論,有興趣的朋友可以參考MSDN:http://msdn.microsoft.com/en-us/library/aa983613(v=VS.71).aspx

 

 

是否可以通過Style來實現對Grid行和列的定制

雖然確實有很多人懷念Windows Forms那種經典的界面開發,但時光之輪總是催我們向前。回到現在我們在用的WPF這個技術,對於控件和界面重用,WPF提供了很強大的Style功能。所以,基於上面這樣的需求,我們會很自然地想到是否可以通過Style來實現。

        <Style TargetType="Grid">
            <Setter Property="ColumnDefinitions">
                <Setter.Value>
                    <ColumnDefinition Width="*"></ColumnDefinition>
                    <ColumnDefinition Width="100"></ColumnDefinition>
                </Setter.Value>
            </Setter>
            <Setter Property="RowDefinitions">
                <Setter.Value>
                    <RowDefinition Height="80"></RowDefinition>
                    <RowDefinition Height="*"></RowDefinition>
                </Setter.Value>
            </Setter>
        </Style>

如果能這樣寫,當然是很好的。但問題是,你不能這樣寫。錯誤如下


image

如果去查看Grid的類型定義,會發現這個屬性確實只有get方法器

image

 

 

是否可以通過對Grid進行擴展來實現行和列的定制

一計不成,我們可以繼續想辦法。有着良好的面向對象素養的同學一定可以想到,那么要不就對Grid做一個擴展,默認提供行和列的定義,這樣行不行呢?例如

using System.Windows.Controls;

namespace WpfApplicationSample
{
    class LayoutGrid:Grid
    {


        protected override void OnInitialized(System.EventArgs e)
        {
            base.OnInitialized(e);

            //默認提供兩行兩列的實現
            base.ColumnDefinitions.Add(new ColumnDefinition());
            base.ColumnDefinitions.Add(new ColumnDefinition() { Width = new System.Windows.GridLength(100) });

            base.RowDefinitions.Add(new RowDefinition() { Height = new System.Windows.GridLength(80) });
            base.RowDefinitions.Add(new RowDefinition());
        }
    }
}
然后,我們在界面上可以像下面這樣使用。很明顯,這樣可以簡化很多了。
<Window x:Class="WpfApplicationSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:extension="clr-namespace:System.Windows.Extensions;assembly=WPFExtension"
        xmlns:local="clr-namespace:WpfApplicationSample"
        Title="MainWindow"
        Height="350"
        Width="525">
    
    <Window.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="FontSize"
                    Value="30"></Setter>
        </Style>
    </Window.Resources>
    
    
    <local:LayoutGrid extension:GridHelper.ShowBorder="true">
        <!--頂部控件-->
        <ContentControl>
            <ContentControl.Content>
                <TextBlock Text="Top"></TextBlock>
            </ContentControl.Content>
        </ContentControl>


        <!--主體控件-->
        <ContentControl Grid.Row="1">
            <ContentControl.Content>
                <TextBlock Text="Main"></TextBlock>
            </ContentControl.Content>
        </ContentControl>

        <!--右側控件-->
        <ContentControl Grid.Column="1"
                        Grid.RowSpan="2">
            <ContentControl.Content>
                <TextBlock Text="Right"></TextBlock>
            </ContentControl.Content>
        </ContentControl>

    </local:LayoutGrid>

</Window>

 

這個方案的美中不足,在於使用者還是需要記住要在具體的內容控件上面設置Grid.Row,Grid.Column等相關信息,而且,如果他忘記寫,或者沒有按照規范寫,實際上就沒有實現界面的統一,因為你沒有辦法強制他必須寫,或者怎么寫。

要想實現界面的統一,就必須把這種可能改變界面的屬性,從使用者的身邊移開。

 

使用用戶控件實現PlaceHolder式的控件模板

既然直接擴展Grid並不能完整地實現我們的需求,我接下來想到是否可以使用用戶控件來封裝,而且為了讓用戶使用簡單,同時也避免用戶不按照規定使用,所以我借鑒了ASP.NET中的母版頁(master page)這樣的技術,使用類似PlaceHolder這樣的方式來實現了一個控件模板。

<UserControl x:Class="WpfApplicationSample.TemplateLayout"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:extension="clr-namespace:System.Windows.Extensions;assembly=WPFExtension"
             mc:Ignorable="d"
             d:DesignHeight="300"
             d:DesignWidth="300"
             x:Name="root">
    <Grid extension:GridHelper.ShowBorder="True">
        <Grid.RowDefinitions>
            <RowDefinition Height="80"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"></ColumnDefinition>
            <ColumnDefinition Width="100"></ColumnDefinition>
        </Grid.ColumnDefinitions>

        <!--頂部控件-->
        <ContentControl Content="{Binding TopControl,ElementName=root}">
        </ContentControl>


        <!--主體控件-->
        <ContentControl Grid.Row="1"
                        Content="{Binding MainControl,ElementName=root}">
        </ContentControl>

        <!--右側控件-->
        <ContentControl Grid.Column="1"
                        Grid.RowSpan="2"
                        Content="{Binding RightControl,ElementName=root}">
        </ContentControl>
    </Grid>
</UserControl>

需要注意的是,因為這里是定義一個控件模板,所以具體的內容控件是沒有內容的,我們希望使用者可以后期再插入具體的內容。這里使用到了綁定的技術。為了支持綁定,我們在這個控件中添加了三個依賴屬性,如下

using System.Windows;
using System.Windows.Controls;

namespace WpfApplicationSample
{
    /// <summary>
    /// Interaction logic for TemplateLayout.xaml
    /// </summary>
    public partial class TemplateLayout : UserControl
    {
        public TemplateLayout()
        {
            InitializeComponent();
        }




        public object TopControl
        {
            get { return (object)GetValue(TopControlProperty); }
            set { SetValue(TopControlProperty, value); }
        }

        // Using a DependencyProperty as the backing store for TopControl.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty TopControlProperty =
            DependencyProperty.Register("TopControl", typeof(object), typeof(TemplateLayout), new PropertyMetadata(null));





        public object MainControl
        {
            get { return (object)GetValue(MainControlProperty); }
            set { SetValue(MainControlProperty, value); }
        }

        // Using a DependencyProperty as the backing store for MainControl.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty MainControlProperty =
            DependencyProperty.Register("MainControl", typeof(object), typeof(TemplateLayout), new PropertyMetadata(null));





        public object RightControl
        {
            get { return (object)GetValue(RightControlProperty); }
            set { SetValue(RightControlProperty, value); }
        }

        // Using a DependencyProperty as the backing store for RightControl.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty RightControlProperty =
            DependencyProperty.Register("RightControl", typeof(object), typeof(TemplateLayout), new PropertyMetadata(null));


    }
}

 

那么,如何來使用這個模板控件呢?大致是下面這樣的:

    <local:TemplateLayout>
        <local:TemplateLayout.TopControl>
            <TextBlock Text="Top"></TextBlock>
        </local:TemplateLayout.TopControl>
        <local:TemplateLayout.MainControl>
            <TextBlock Text="Main"></TextBlock>
        </local:TemplateLayout.MainControl>
        <local:TemplateLayout.RightControl>
            <TextBlock Text="Right"></TextBlock>
        </local:TemplateLayout.RightControl>
    </local:TemplateLayout>

很顯然,這樣使用起來更加簡潔方便,而且使用者只能往我們預先定義好的三個內容區域中去填充內容,而不可能因為錯誤的設置屬性(或者忘記設置屬性)而導致界面布局不一致。

image

 

看起來相當不錯,幾乎已經完全實現了我們的要求。我以前也就一直這樣用,直到最近發現一個問題。

如果我們出於一些目的,希望給這些內容控件添加名稱,以便在后台代碼中訪問到它。例如下面這樣:

    <local:TemplateLayout>
        <local:TemplateLayout.TopControl>
            <TextBlock Text="Top" x:Name="topTextBlock"></TextBlock>
        </local:TemplateLayout.TopControl>
        <local:TemplateLayout.MainControl>
            <TextBlock Text="Main"></TextBlock>
        </local:TemplateLayout.MainControl>
        <local:TemplateLayout.RightControl>
            <TextBlock Text="Right"></TextBlock>
        </local:TemplateLayout.RightControl>
    </local:TemplateLayout>

看起來是一個很平常的修改,但你立即會發現,這會導致無法編譯通過。

image

Cannot set Name attribute value 'topTextBlock' on element 'TextBlock'. 'TextBlock' is under the scope of element 'TemplateLayout', which already had a name registered when it was defined in another scope.

這個錯誤實在是讓人捉摸不透,目前也沒有找到合理的解釋。可能的解釋是這樣:因為我們的TextBlock其實是嵌入到TemplateLayout中,而TemplateLayout默認會為內部的控件生成一個名稱,所以就不允許再定義名稱。

為此,我也找了很多資料,一個最接近的討論在這里

http://stackoverflow.com/questions/5758342/how-to-create-wpf-usercontrol-which-contains-placeholders-for-later-usage

這個問題描述到了和本文比較接近的情況,而且也提到了如果假如名稱,會出現錯誤。但是該文也沒有可用的解決方案。

 

針對這個問題,我做了不少研究,同時也找了一些朋友進行討論。其中和韋恩卑鄙 的討論中,他給了我一個啟發,打開了我另外一個思路:如果說定義名稱是必須的,那么既然WPF內部的命名規范無法通過,那么是否可以通過自己的一種什么機制來定義名稱呢?最終我確定使用附加屬性來實現了該功能。榮譽屬於韋恩卑鄙

 

通過附加屬性來為控件添加名稱

我創建了如下這樣一個類型,添加了一個附加屬性,並且為控件查找提供了一個方法。

using System.Collections.Generic;
using System.Linq;
using System.Windows;

namespace WpfApplicationSample
{
    public class LayoutExtension
    {
        /// <summary>
        /// 這個方法用來獲取控件
        /// </summary>
        /// <typeparam name="T">指定控件類型,例如TextBlock</typeparam>
        /// <param name="name">指定控件名稱</param>
        /// <returns></returns>
        public static T GetControl<T>(string name)
            where T : class
        {
            return controls.FirstOrDefault(t => t.Key == name).Value as T;
        }


        private static Dictionary<string, DependencyObject> controls;
        static LayoutExtension()
        {
            controls = new Dictionary<string, DependencyObject>();
        }

        public static string GetName(DependencyObject obj)
        {
            return (string)obj.GetValue(NameProperty);
        }

        public static void SetName(DependencyObject obj, string value)
        {
            obj.SetValue(NameProperty, value);

        }

        // Using a DependencyProperty as the backing store for Name.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty NameProperty =
            DependencyProperty.RegisterAttached("Name", typeof(string), typeof(LayoutExtension), new PropertyMetadata(string.Empty, (d, e) =>
            {
                if (e.NewValue != null)
                {
                    var name = e.NewValue.ToString();

                    if (!controls.ContainsKey(name))
                        controls.Add(name, d);
                }
            }));

    }
}

如何使用這個擴展呢?

    <local:TemplateLayout>
        <local:TemplateLayout.TopControl>
            <TextBlock Text="Top"
                       local:LayoutExtension.Name="topTextBlock"></TextBlock>
        </local:TemplateLayout.TopControl>
        <local:TemplateLayout.MainControl>
            <TextBlock Text="Main"></TextBlock>
        </local:TemplateLayout.MainControl>
        <local:TemplateLayout.RightControl>
            <TextBlock Text="Right"></TextBlock>
        </local:TemplateLayout.RightControl>
    </local:TemplateLayout>

然后,如果需要在后台代碼中訪問這個控件,就可以大致像下面這樣操作

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace WpfApplicationSample
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            Loaded += MainWindow_Loaded;
        }

        void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            var txt = LayoutExtension.GetControl<TextBlock>("topTextBlock");
            txt.Foreground = new SolidColorBrush(Colors.Red);

        }
    }
}

image

 

如何更加方便地使用這個擴展?

為了使得大家更加方便地使用這個擴展,我將其合並到了我之前寫過的針對WPF和Silverlight的擴展包中,大家可以通過nuget package manager搜索wpfsilverlightextension下載安裝這個擴展包

image

目前這個擴展,既支持WPF,也支持Silverlight。可以免費使用。

如果是使用這個包的話,那么在導入名稱空間的時候,我習慣用extension這個名稱,所以在具體頁面中用的時候,大致上是下面這樣

    <local:TemplateLayout>
        <local:TemplateLayout.TopControl>
            <TextBlock Text="Top"
                       extension:LayoutExtension.Name="topTextBlock"></TextBlock>
        </local:TemplateLayout.TopControl>
        <local:TemplateLayout.MainControl>
            <TextBlock Text="Main"></TextBlock>
        </local:TemplateLayout.MainControl>
        <local:TemplateLayout.RightControl>
            <TextBlock Text="Right"></TextBlock>
        </local:TemplateLayout.RightControl>
    </local:TemplateLayout>
 
 

是否一定要使用這個擴展?

看起來還不錯,但是是否一定要使用這個擴展呢?並不見得。因為我之前就說過,我以前沒有意識到這個Name會出問題,是因為我幾乎大部分時候都沒有命名這種需要。為什么呢?因為我們大部分時候都會使用mvvm這種模式進行WPF應用程序的開發,在這種情況下,我們是不會在代碼中去訪問到控件的。例如下面是一個較為真實的例子:

<Page x:Class="EmployeePage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:local="clr-namespace:SMSApplicationSample"
      xmlns:extension="clr-namespace:System.Windows.Extensions;assembly=WPFExtension"
      xmlns:tk="clr-namespace:Xceed.Wpf.Toolkit;assembly=WPFToolkit.Extended"
      mc:Ignorable="d"
      d:DesignHeight="300"
      d:DesignWidth="300"
      Title="EmployeePage">

    <Page.DataContext>
        <local:EmployeePageViewModel></local:EmployeePageViewModel>
    </Page.DataContext>

    <tk:BusyIndicator 
        IsBusy="{Binding IsBusy}"
        BusyContent="Data loading,pls wait...">
       
        <local:TemplateLayout>
            <local:TemplateLayout.Condition>
                <!--加載所有員工-->
                <DataGrid ItemsSource="{Binding Employees}"
                          SelectedItem="{Binding CurrentEmployee,Mode=TwoWay}">
                    <!--通過前台的選擇,TwoWay的方式可以將CurrentEmployee的更新告訴后台,並且去更新綁定了該屬性的元素,這個機制就是所謂的依賴屬性的通知功能-->
                </DataGrid>
            </local:TemplateLayout.Condition>
            <local:TemplateLayout.Result>
                <!--顯示一個員工-->
                <ContentControl Content="{Binding CurrentEmployee}">
                    <ContentControl.ContentTemplate>
                        <DataTemplate>
                            <Grid extension:GridHelper.ShowBorder="True">
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="auto"></RowDefinition>
                                    <RowDefinition Height="auto"></RowDefinition>
                                    <RowDefinition Height="auto"></RowDefinition>
                                    <RowDefinition Height="auto"></RowDefinition>
                                </Grid.RowDefinitions>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="100"></ColumnDefinition>
                                    <ColumnDefinition Width="*"></ColumnDefinition>
                                    <ColumnDefinition Width="100"></ColumnDefinition>
                                </Grid.ColumnDefinitions>

                                <Image Source="{Binding Photo}"
                                       Grid.Column="2"
                                       Grid.RowSpan="4"></Image>

                                <TextBlock Text="FirstName:"></TextBlock>
                                <TextBlock Text="{Binding FirstName}"
                                           Grid.Column="1"></TextBlock>

                                <TextBlock Text="LastName:"
                                           Grid.Row="1"></TextBlock>
                                <TextBlock Text="{Binding LastName}"
                                           Grid.Row="1"
                                           Grid.Column="1"></TextBlock>

                                <TextBlock Text="Title:"
                                           Grid.Row="2"></TextBlock>
                                <TextBlock Text="{Binding Title}"
                                           Grid.Row="2"
                                           Grid.Column="1"></TextBlock>

                                <TextBlock Text="Address:"
                                           Grid.Row="3"></TextBlock>
                                <TextBlock Text="{Binding Address}"
                                           Grid.Row="3"
                                           Grid.Column="1"></TextBlock>

                            </Grid>
                        </DataTemplate>
                    </ContentControl.ContentTemplate>

                </ContentControl>
            </local:TemplateLayout.Result>
            <local:TemplateLayout.Action>
                <!--顯示一些按鈕,可以添加,刪除,修改員工-->
                <StackPanel>
                    <Button Content="New..."
                            Command="{Binding NewCommand}"></Button>
                    <Button Content="Update..."
                            Command="{Binding UpdateCommand}"
                            CommandParameter="{Binding CurrentEmployee}"></Button>
                    <Button Content="Delete..."
                            Command="{Binding DeleteCommand}"></Button>

                    <Button Content="Show Report..."
                            Command="{Binding ReportCommand}"></Button>
                </StackPanel>
            </local:TemplateLayout.Action>
        </local:TemplateLayout>
    </tk:BusyIndicator>
        
</Page>

 

 

本文源代碼可以通過 http://files.cnblogs.com/chenxizhang/WpfApplicationSample.zip  下載


免責聲明!

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



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