站点图标 江湖人士

JHRS开发框架之公用组件WPF用户控件封装

JHRS开发框架之公用组件WPF用户控件封装

继上一篇介绍了怎样封装ViewModel的基类,但随着项目大了,一个功能点一个功能点的做,真的累,很多系统里面,在局部有很多相似的功能,数据展示几乎一样的,或许不一样的只是摆放的位置,显示的样式不同罢了;这种东西,一个功能一个功能的实现,那就有点朝着996的状态发展了;因此在JHRS框架中,也体现了懒人干活的思想,那就是能封装成控件的,坚决搞成一个控件,供大家享乐。

JHRS开发框架之公用组件WPF用户控件封装

当然,用户控件可以套在用户控件里面,一个拥有复杂功能的页面,可以由众多的用户控件组成,最终你会发现,用户控件封装得越优雅,做复杂的功能页面也不会很难了,只需要像搭积木那样,把控件丢上去,数据绑定上就完事了,最后如果要调整样式,稍微调整下就OK了。

WPF用户控件封装

因为框架中引入了Prism将各子系统模块化了,所以我们在WPF用户控件封装的时候,分为两种情况,一种是封装整个系统公用的用户控件,另外一种是各子模块自己的用户控件;整个系统公用的用户控件,需要保持着高扩展性,灵活性,即使后期对该控件增加功能,也要尽量做到不影响已经使用该控件的页面(Page),还需要让使用的页面可以灵活的设置一些属性以满足不同页面(Page)的功能需求。

在框架中只封装了3个基本的用户控件,动态列的DataGrid,可调接口的Combobox,动态分页表格。

JHRS开发框架动态列的DataGrid

大部分管理系统,都离不开表格展示数据,而WPF项目中,基本上都会使用DataGrid来展示数据;熟悉WPF开发的朋友都知道,如果手工撸一个表格,那代码贼烦人,需要一个列一个列的写代码并绑定数据;而在JHRS框架中提供的思路是基于注解的方式(自定义BindDescriptionAttribute类用于描述每列)动态生成每一列数据并自动绑定数据,对于复杂的列,如某一列里面展示为下拉框(ComoboBox)或者更复杂的展示,只需要在资源(Resources)里面定义DataTemplate即可,然后动态加载就可以了。对于最每一行的操作列,也是一样的套路。

动态列的DataGrid封装思路是:编写一个DataGridEx类,继承自DataGrid类,在DataGridEx类中,需要定义一个依赖属性我们称为DataSource,用它来绑定数据,然后在DataSourceProperty的回调函数里面把DataSource赋值给原本的 ItemSource属性,最后重写OnInitialized方法,将数据源传入DataGrid的扩展方法GenerateColumns动态生成列就完成了WPF用户控件封装,详见下方代码。

DataGridEx类源码

/// <summary>
    /// 輕量級的DataGrid擴展
    /// </summary>
    public class DataGridEx : DataGrid
    {
        /// <summary>
        /// 構造函數
        /// </summary>
        public DataGridEx()
        {
            this.AutoGenerateColumns = false;
            this.Loaded += DataGridEx_Loaded;
            this.LoadingRow += PagingDataList_LoadingRow;
        }

        /// <summary>
        /// 给表格添加样式
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void DataGridEx_Loaded(object sender, RoutedEventArgs e)
        {
            this.CanUserAddRows = false;
        }

        /// <summary>
        /// 生成序号
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void PagingDataList_LoadingRow(object sender, DataGridRowEventArgs e)
        {
            if (EnableRowNumber)
                //需要分页
                e.Row.Header = e.Row.GetIndex() + 1;
        }

        /// <summary>
        /// 操作列key
        /// </summary>
        public string OperatingKey { get; set; } = string.Empty;
        /// <summary>
        /// 操作列的宽度
        /// </summary>
        public DataGridLength OperationWidth { get; set; }

        /// <summary>
        /// 是否启用序号
        /// </summary>
        public bool EnableRowNumber { get; set; } = true;

        /// <summary>
        /// 禁止显示的列
        /// </summary>
        public string DisableCloumn { get; set; }

        public IEnumerable<object> DataSource
        {
            get { return (IEnumerable<object>)GetValue(DataSourceProperty); }
            set { SetValue(DataSourceProperty, value); }
        }

        private bool IsGenerateColumns = false;
        // Using a DependencyProperty as the backing store for DataSource.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DataSourceProperty =
            DependencyProperty.Register("DataSource", typeof(IEnumerable<object>), typeof(DataGridEx), new PropertyMetadata((d, e) =>
            {
                DataGridEx u = d as DataGridEx;
                u.ItemsSource = u.DataSource;

                if (u.IsGenerateColumns || u.DataSource == null ) return;
                var index = 0;
                if (u.EnableRowNumber)
                {
                    var acolumn = new DataGridTextColumn
                    {
                        Header = "序号",
                        Width = new DataGridLength(50),
                        Binding = new Binding("Header") { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(DataGridRow), 1) }
                    };
                    u.Columns.Insert(0, acolumn);
                    index++;
                }
                u.GenerateColumns(index, u.ItemsSource, u.OperatingKey, u.OperationWidth);
                u.IsGenerateColumns = true;

            }));


        //protected override void OnInitialized(EventArgs e)
        //{
        //    if (IsGenerateColumns || ItemsSource == null) return;
        //    var index = 0;
        //    if (EnableRowNumber)
        //    {
        //        var acolumn = new DataGridTextColumn
        //        {
        //            Header = "序号",
        //            Width = new DataGridLength(50),
        //            Binding = new Binding("Header") { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(DataGridRow), 1) }
        //        };
        //        this.Columns.Insert(0, acolumn);
        //        index++;
        //    }
        //    this.GenerateColumns(index, ItemsSource, OperatingKey, OperationWidth);
        //    IsGenerateColumns = true;
        //}
    }

以上的基本上是完整的源码,github是参见这里

DataGridExtensions扩展类源码

/// <summary>
    /// DataGrid扩展方法
    /// </summary>
    public static class DataGridExtensions
    {
        /// <summary>
        /// 动态生成列
        /// </summary>
        /// <param name="dataGrid">DataGrid控件实例</param>
        /// <param name="index">列插入位置</param>
        /// <param name="data">数据源</param>
        /// <param name="operationKey">操作列资源</param>
        /// <param name="operationWidth">操作列宽度</param>
        public static void GenerateColumns(this DataGrid dataGrid, int index, object data, string operationKey, DataGridLength operationWidth)
        {
            IList<BindDescriptionAttribute> list = GetColumns(data);
            //Window win = Application.Current.Windows.OfType<Window>().SingleOrDefault(x => x.IsActive);
            //Page page = win.GetChildObject<Page>("page");
            //if (page == null) throw new Exception("未獲取到當前窗口名稱爲page的(Page)頁面對象,原因:沒有爲Page設置Name,且名稱必須爲【page】!");

            Page page = GetParentObject<Page>(dataGrid, "page");

            for (int i = 0; i < list.Count; i++)
            {
                switch (list[i].ShowAs)
                {
                    case ShowScheme.普通文本:
                        dataGrid.Columns.Insert(i + index, new DataGridTextColumn
                        {
                            Header = list[i].HeaderName,
                            Binding = new Binding(list[i].PropertyName),
                            Width = list[i].Width
                        });
                        break;
                    case ShowScheme.自定义:
                        if (page.FindResource(list[i].ResourceKey) != null)
                        {
                            DataGridTemplateColumn val = new DataGridTemplateColumn();
                            val.Header = list[i].HeaderName;
                            val.Width = list[i].Width;
                            val.CellTemplate = page.FindResource(list[i].ResourceKey) as DataTemplate;
                            dataGrid.Columns.Insert(i + index, val);
                        }
                        break;
                }
            }
            if (!string.IsNullOrWhiteSpace(operationKey) && page != null)
            {
                var resource = page.FindResource(operationKey);
                if (resource!=null)
                {
                   
                    var col = new DataGridTemplateColumn() { Header = "操作", Width = operationWidth };
                    col.CellTemplate = resource as DataTemplate;
                    dataGrid.Columns.Add(col);
                }
            }
        }

        /// <summary>
        /// 获取数据源对象到列的映射关系
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        private static IList<BindDescriptionAttribute> GetColumns(object data)
        {
            List<BindDescriptionAttribute> list = new List<BindDescriptionAttribute>();
            var pros = data.GetType().GenericTypeArguments[0].GetProperties();
            foreach (var item in pros)
            {
                var a = item.GetCustomAttribute<BindDescriptionAttribute>();
                if (a != null) { a.PropertyName = item.Name; list.Add(a); }
            }
            return list.OrderBy(x => x.DisplayIndex).ToArray();
        }

        /// <summary>
        /// 查找父级控件
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="obj"></param>
        /// <param name="name"></param>
        /// <returns></returns>
        public static T GetParentObject<T>(DependencyObject obj, string name) where T : FrameworkElement
        {
            DependencyObject parent = VisualTreeHelper.GetParent(obj);

            while (parent != null)
            {
                if (parent is T && (((T)parent).Name == name | string.IsNullOrEmpty(name)))
                {
                    return (T)parent;
                }

                parent = VisualTreeHelper.GetParent(parent);
            }

            return null;
        }
    }

完整的参见这里

BindDescriptionAttribute注解类

在调用web api从服务器端返回的数据中,需要在本地的WPF项目定义相关的实体类,并且在实体类的属性上标记绑定描述类(BindDescriptionAttribute),这个类直接描述了展面如何展示(一般用DataGrid,可按此思路扩展不规则表单),这也是WPF用户控件封装之前的准备工作。

/// <summary>
    /// DataGrid绑定数据源描述
    /// </summary>
    public class BindDescriptionAttribute : Attribute
    {
        /// <summary>
        /// 列名
        /// </summary>
        public string HeaderName { get; set; }

        /// <summary>
        /// 显示为
        /// </summary>
        public ShowScheme ShowAs { get; set; }

        /// <summary>
        /// 显示顺序
        /// </summary>
        public int DisplayIndex { get; set; }

        /// <summary>
        /// DataGrid列绑定属性名称
        /// </summary>
        public string PropertyName { get; set; }

        /// <summary>
        /// 应用内的容模板Key
        /// </summary>
        public string ResourceKey { get; set; }

        /// <summary>
        /// 列宽
        /// </summary>
        public DataGridLength Width { get; set; }

        /// <summary>
        /// 列宽ByGrid
        /// </summary>
        public GridLength CloumnWidth { get; set; }


        /// <summary>
        /// DataGrid绑定数据源描述
        /// </summary>
        /// <param name="headerName">列名</param>
        /// <param name="showAs">显示为</param>
        /// <param name="width">宽度</param>
        /// <param name="displayIndex">显示顺序</param>
        /// <param name="resourceKey">自定义列Key</param>
        public BindDescriptionAttribute(string headerName, ShowScheme showAs = ShowScheme.普通文本, string width = "Auto", int displayIndex = 0, string resourceKey = "")
        {
            HeaderName = headerName;
            DisplayIndex = displayIndex;
            ResourceKey = resourceKey;
            ShowAs = showAs;
            var convert = new DataGridLengthConverter();
            Width = (DataGridLength)convert.ConvertFrom(width);
            var gridCOnvert = new GridLengthConverter();
            CloumnWidth = (GridLength)gridCOnvert.ConvertFrom(width);

            if (showAs == ShowScheme.自定义 && string.IsNullOrWhiteSpace(resourceKey))
                throw new ArgumentException($"自定义列时需要指定{nameof(resourceKey)}参数!");
        }
    }

    /// <summary>
    /// 展示方式
    /// </summary>
    public enum ShowScheme
    {
        普通文本 = 1,
        自定义 = 4
    }

上方的枚举ShowScheme则描述了对应的列的显示方案,是该使用普通的文本还是加载数据模板(DataTemplate)。

如何使用DataGridEx实现动态表格功能

要使用自己扩展的动态DataGrid其实是跟使用常规的DataGrid是一样的,只是区别是绑定数据是使用DataSource属性而已,下面就是WPF用户控件封装之动态表格xaml,如下代码所示:

<Page
    x:Class="JHRS.RegisterManagement.Views.RegisterList"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:prism="http://prismlibrary.com/"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:c="clr-namespace:JHRS.Core.Controls.Common;assembly=JHRS.Core"
    xmlns:b="http://schemas.microsoft.com/xaml/behaviors" 
    prism:ViewModelLocator.AutoWireViewModel="True" 
    mc:Ignorable="d" 
    d:DesignHeight="450" d:DesignWidth="800"
    x:Name="page"
    Title="RegisterList" Background="White">
    
    <b:Interaction.Triggers>
        <b:EventTrigger EventName="Loaded" >
            <b:InvokeCommandAction Command="{Binding LoadedCommand}" CommandParameter="{Binding ElementName=page}" />
        </b:EventTrigger>
    </b:Interaction.Triggers>
    <Grid x:Name="maskContainer">
        <c:DataGridEx DataSource="{Binding PageData}" IsReadOnly="True"/>
    </Grid>
</Page>

上面代中<c:DataGridEx DataSource=”{Binding PageData}” IsReadOnly=”True”/>这行就是WPF用户控件封装的动态表格绑定数据的用法,而PageData则是来自ViewModel定义的一个IEnumerable<object>属性,只需要在当前页面(Page)的ViewModel里面调用接口获取数据给PageData赋值就可以了,如下代码所示:

        /// <summary>
        /// 綁定分頁數據
        /// </summary>
        [WaitComplete]
        protected async override Task<object> BindPagingData()
        {
            List<Account> list = new List<Account>();
            for (int i = 0; i < 15; i++)
            {
                list.Add(new Account
                {
                    Name = "趙佳仁" + i,
                    RegTime = DateTime.Now.AddDays(i),
                    RoleName = "管理員" + i,
                    Title = "無職" + i,
                    UserID = 100 + i
                });
            }
            PageData = list;
            await Task.Delay(200);
            return true;
        }

而Account类则是这样定义的。

    public class Account
    {
        [BindDescription("用戶ID")]
        public int UserID { get; set; }
        [BindDescription("用戶名")]
        public string Name { get; set; }
        [BindDescription("註冊時間")]
        public DateTime RegTime { get; set; }
        [BindDescription("角色名穩")]
        public string RoleName { get; set; }
        [BindDescription("職級")]
        public string Title { get; set; }
    }

完整的代码参见这里。下图是最终的效果:

WPF用户控件封装

Combobox扩展控件

原生的Combobox是WPF提供的下拉框控件,如果约定在整个系统里面,所有的下拉框控件默认项为请选择,如下图所示:

WPF用户控件封装

统一的给加上这个的话,在框架中是这样做的,先自定义一个BaseComboBox类,继承自ComboBox类,并在代码里面添加一个默认项,代码如下:

using JHRS.Http;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;

namespace JHRS.Core.Controls.Common
{
/// /// 下拉框控件基类 ///
public abstract class BaseComboBox : ComboBox
{
/// /// 下拉框数据源 ///
public IList Data
{
get { return (IList)GetValue(DataProperty); }
set
{
AddDefault(value);
SetValue(DataProperty, value);
}
}

    /// <summary>
    /// 已登录获取到Token客户端对象
    /// </summary>
    protected HttpClient AuthClient => AuthHttpClient.Instance;

    /// <summary>
    /// 服务器配置
    /// </summary>
    protected string BaseUrl => AuthHttpClient.Instance.BaseAddress!.ToString();

    // Using a DependencyProperty as the backing store for Data.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty DataProperty =
        DependencyProperty.Register("Data", typeof(IList), typeof(BaseComboBox), new PropertyMetadata(null, (d, e) =>
        {
            BaseComboBox c = (BaseComboBox)d;
            var list = e.NewValue as IList;
            if (list != null)
                c.AddDefault(list);
            c.Data = list;
            c.ItemsSource = list;
        }));

    /// <summary>
    /// 构造函数
    /// </summary>
    public BaseComboBox()
    {
        this.Initialized += OnInitialized;
    }

    /// <summary>
    /// 下拉框初始化事件,子类实现,可以加载各自数据。
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    protected abstract void OnInitialized(object sender, EventArgs e);

    private string DefaultSelectedValue = "-1";
    private string DefaultSelectedText = "—請選擇—";

    /// <summary>
    /// 添加默认项:请选择
    /// </summary>
    /// <param name="data"></param>
    private void AddDefault(IList data)
    {
        if (data == null || data.Count == 0) return;
        var pros = data[0].GetType().GetProperties();
        bool hasSelect = false;
        var s = pros.FirstOrDefault(x => x.Name == SelectedValuePath);
        var d = pros.FirstOrDefault(x => x.Name == DisplayMemberPath);
        if (s == null) throw new Exception("未給ComboBox指定SelectedValuePath屬性,注意:屬性區分大小寫!");
        if (d == null) throw new Exception("未给ComboBox指定DisplayMemberPath屬性,注意:屬性區分大小寫!");
        foreach (var item in data)
        {
            if (s == d && (s.GetValue(item, null) + "") == DefaultSelectedText)
            {
                hasSelect = true;
                break;
            }
            else if ((s.GetValue(item, null) + "") == DefaultSelectedValue && (d.GetValue(item, null) + "") == DefaultSelectedText)
            {
                hasSelect = true;
                break;
            }
        }
        if (hasSelect == false)
        {
            var subType = data.GetType().GenericTypeArguments[0];
            if (subType.Name.StartsWith("<>f__AnonymousType")) return;
            var m = Activator.CreateInstance(subType);
            if (s != d)
            {
                s.SetValue(m, Convert.ChangeType(DefaultSelectedValue, s.PropertyType), null);
                d.SetValue(m, Convert.ChangeType(DefaultSelectedText, d.PropertyType), null);
            }
            else
            {
                d.SetValue(m, Convert.ChangeType(DefaultSelectedText, d.PropertyType), null);
            }
            data.Insert(0, m);
        }
    }
}

}

上面的就是完整代码需要注意的是,在上面的代码中,处理匿名类型有Bug,并没有修复,强类型的绑定是会添加【—請選擇—】这个默认项的

业务相关的ComboBox下拉框

在实际项目里面,会有很多下拉框的数据源是需要调用接口获取的,将它封装后就可以避免在很多的功能页面(Page)或者控件里面再调接口来获取数据给ComboBox绑定数据,因此封装后直接拖过去用就完事了,这是WPF用户控件封装之下拉框。

在框架里面封装了像科室,字典,通用业务状态的下拉框,这里只放一个科室的下拉框示例代码,因为科室是需要调接口获取的,下方注释掉的WPF用户控件封装代码就是真实项目中调用接口获取数据来绑定的代码。

using JHRS.Core.Controls.Common;
using JHRS.Core.Models;
using System;
using System.Collections.Generic;

namespace JHRS.Core.Controls.DropDown
{
    /// <summary>
    /// 科室下拉框
    /// </summary>
    public class DepartmentComboBox : BaseComboBox
	{
		/// <summary>
		/// 初始化科室數據,可調用接口獲取數據。
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		protected override void OnInitialized(object sender, EventArgs e)
		{
			this.DisplayMemberPath = "Name";
			this.SelectedValuePath = "Id";

			//var response = await RestService.For<IDepartmentApi>(AuthHttpClient.Instance).GetAll();
			//if (response.Succeeded)
			//{
			//	Data = response.Data as IList;
			//}

			List<DepartmentOutputDto> list = new List<DepartmentOutputDto>();
			for (int i = 0; i < 20; i++)
			{
				list.Add(new DepartmentOutputDto
				{
					Id = i + 1,
					Name = $"測試科室{i + 1}"
				});
			}
			base.Data = list;
		}
	}

}

封装后下拉框怎样使用

如下图所示一样,在演示框架中,WPF用户控件封装之后的控件是直接将其拖到用户控件相关位置,然后绑定你需要获取的值即可,使用的地方不需要关注科室的数据从哪儿来的,你只需要知道你用什么属性取接收选中的值即可。

WPF用户控件封装

以上就是使用的方法,接下来看看如何封装分页表格。

动态分页表格

每个系统里面分页表格是大头,或者说是比较复杂的功能,而在框架里面是将表格和分页控件封装到一起形成一个用户控件,在需要使用的地方,也是直接拖过去,并调用分页接口获取数据绑定即可;WPF用户控件封装表格的每一列展示什么数据,也是采用最上面介绍的最基础的动态表格思想解决的。

分页表格控件XAML代码

<UserControl x:Class="JHRS.Core.Controls.DataGrids.PagingDataGrid" 
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
             xmlns:b="http://schemas.microsoft.com/xaml/behaviors" 
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:hc="https://handyorg.github.io/handycontrol" 
             xmlns:local="clr-namespace:JHRS.Core.Controls.DataGrids" 
             Loaded="UserControl_Loaded">
    <Grid Name="ucDataGrid">
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition Height="5"/>
            <RowDefinition Height="30"/>
        </Grid.RowDefinitions>
        <DataGrid  x:Name="pagingDataList" Grid.Row="0" IsReadOnly="True"
                  ItemsSource="{Binding PageData}" CanUserAddRows="False" SelectionMode="Single" 
                  LoadingRow="pagingDataList_LoadingRow">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding Header, RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}, Mode=FindAncestor}}"  CanUserSort="False" Header="序号" IsReadOnly="True" Width="40"/>
                <DataGridTemplateColumn Width="40" x:Name="isCheckbox">
                    <DataGridTemplateColumn.Header>
                        <CheckBox Click="CheckBox_Click"></CheckBox>
                    </DataGridTemplateColumn.Header>
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <CheckBox Click="chkItem_Click" x:Name="chkItem" VerticalAlignment="Center" HorizontalAlignment="Center"></CheckBox>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>
        <hc:Pagination x:Name="ucPagination" Grid.Row="2" HorizontalAlignment="Right"  MaxPageCount="{Binding PagingData.MaxPageCount}" PageIndex="{Binding PagingData.PageIndex, Mode=TwoWay}">
            <b:Interaction.Triggers>
                <b:EventTrigger EventName="PageUpdated" >
                    <b:InvokeCommandAction Command="{Binding ChangePageIndexCommand}"/>
                </b:EventTrigger>
            </b:Interaction.Triggers>
        </hc:Pagination>
    </Grid>
</UserControl>

在这个WPF用户控件封装的xaml代码中,主要放了两个控件,一个DataGrid表格控件,一个Pagination分页控件。后台的C#代码如下:

using JHRS.Core.Extensions;
using JHRS.Filter;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace JHRS.Core.Controls.DataGrids
{
    /// <summary>
    /// PagingDataGrid.xaml 的交互逻辑
    /// </summary>
    public partial class PagingDataGrid : UserControl
    {
        public PagingDataGrid()
        {
            InitializeComponent();
            pagingDataList.AutoGenerateColumns = false;
        }

        /// <summary>
        /// 生成序号
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void pagingDataList_LoadingRow(object sender, DataGridRowEventArgs e)
        {
            if (EnablePagination)
                //需要分页
                e.Row.Header = (PagingData.PageIndex - 1) * PagingData.PageSize + e.Row.GetIndex() + 1;
            else
                //不需要分页
                e.Row.Header = e.Row.GetIndex() + 1;
        }

        /// <summary>
        /// 表格数据源
        /// </summary>
        public IEnumerable<object> PageData
        {
            get { return (IEnumerable<object>)GetValue(PageDataProperty); }
            set { SetValue(PageDataProperty, value); }
        }

        //是否已经生成了列并建立了绑定关系 
        private bool IsGenerateColumns = false;
        // Using a DependencyProperty as the backing store for PageData.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PageDataProperty =
            DependencyProperty.Register("PageData", typeof(IEnumerable<object>), typeof(PagingDataGrid), new PropertyMetadata((d, e) =>
            {
                PagingDataGrid pagingDataGrid = d as PagingDataGrid;
                if (pagingDataGrid.IsGenerateColumns) return;
                int num = 0;
                if (pagingDataGrid.EnableRowNumber) num++;

                if (pagingDataGrid.EnableCheckBoxColumn) num++;

                pagingDataGrid.pagingDataList.GenerateColumns(num, e.NewValue, pagingDataGrid.OperatingKey, pagingDataGrid.OperatingWidth);
                pagingDataGrid.IsGenerateColumns = true;
            }));


        /// <summary>
        /// 分页控件数据源
        /// </summary>
        public PagingData PagingData
        {
            get { return (PagingData)GetValue(PagingDataProperty); }
            set { SetValue(PagingDataProperty, value); }
        }

        // Using a DependencyProperty as the backing store for PagingData.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PagingDataProperty =
            DependencyProperty.Register("PagingData", typeof(PagingData), typeof(PagingDataGrid));

        /// <summary>
        /// 是否启用分页功能
        /// </summary>
        public bool EnablePagination { get; set; } = true;

        /// <summary>
        /// 是否启用序号
        /// </summary>
        public bool EnableRowNumber { get; set; } = true;

        /// <summary>
        /// 是否复选框列
        /// </summary>
        public bool EnableCheckBoxColumn { get; set; } = false;

        /// <summary>
        /// 复选框列是否启用全选功能
        /// </summary>
        public bool EnableSelectAll { get; set; } = false;

        /// <summary>
        /// 操作列的Key
        /// </summary>
        public string OperatingKey { get; set; }

        /// <summary>
        /// 操作列宽
        /// </summary>
        public DataGridLength OperatingWidth { get; set; }

        /// <summary>
        /// 当前选中数据
        /// </summary>
        public IEnumerable<object> CheckedList
        {
            get { return (IEnumerable<object>)GetValue(SelectedListProperty); }
            set { SetValue(SelectedListProperty, value); }
        }

        // Using a DependencyProperty as the backing store for SelectedList.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SelectedListProperty =
            DependencyProperty.Register("CheckedList", typeof(IEnumerable<object>), typeof(PagingDataGrid),
            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));


        /// <summary>
        /// 初始化表格行为
        /// </summary>
        private void InintBehavior()
        {
            if (!EnablePagination) ucDataGrid.Children.Remove(ucPagination);
            if (!EnableRowNumber) pagingDataList.Columns.Remove(pagingDataList.Columns.FirstOrDefault(x => x.Header.ToString() == "序号"));

            if (!EnableCheckBoxColumn) pagingDataList.Columns.Remove(isCheckbox);
            if (!EnableSelectAll) isCheckbox.Header = "选择";
        }

        /// <summary>
        /// 用户控件初始化
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
            InintBehavior();
        }

        /// <summary>
        /// 复选框全选事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void CheckBox_Click(object sender, RoutedEventArgs e)
        {
            var c = sender as CheckBox;
            CheckedAll(pagingDataList, c.IsChecked);
            CheckedList = GetSelected();
        }

        /// <summary>
        /// 全选
        /// </summary>
        /// <param name="parent"></param>
        /// <param name="isChecked"></param>
        private void CheckedAll(DependencyObject parent, bool? isChecked)
        {
            int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
            for (int i = 0; i < numVisuals; i++)
            {
                DependencyObject v = VisualTreeHelper.GetChild(parent, i);
                CheckBox child = v as CheckBox;

                if (child == null)
                {
                    CheckedAll(v, isChecked);
                }
                else
                {
                    child.IsChecked = isChecked;
                    break;
                }
            }
        }

        /// <summary>
        /// 获取所有选中项
        /// </summary>
        /// <returns></returns>
        private List<object> GetSelected()
        {
            List<object> list = new List<object>();
            foreach (var item in pagingDataList.ItemsSource)
            {
                var m = isCheckbox.GetCellContent(item);
                var c = m.GetChildObject<CheckBox>("chkItem");
                if (c != null && c.IsChecked == true)
                {
                    list.Add(item);
                }
            }
            return list;
        }

        /// <summary>
        /// 单击选中事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void chkItem_Click(object sender, RoutedEventArgs e)
        {
            CheckedList = GetSelected();
        }
    }
}

如何使用动态分页表格

在需要使用的页面(Page)或者控件里面,将WPF用户控件封装后的控件拖进来就可以了,完整的xaml代码如下

<Page x:Class="JHRS.OutpatientSystem.Views.Reservation" 
      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:JHRS.OutpatientSystem.Views" 
      xmlns:prism="http://prismlibrary.com/" 
      xmlns:b="http://schemas.microsoft.com/xaml/behaviors" 
      prism:ViewModelLocator.AutoWireViewModel="True" 
      xmlns:f="clr-namespace:JHRS.Core.Controls.Layouts;assembly=JHRS.Core" 
      xmlns:p="clr-namespace:JHRS.Core.Controls.DataGrids;assembly=JHRS.Core" 
      xmlns:c="clr-namespace:JHRS.Core.Controls.DropDown;assembly=JHRS.Core" 
      x:Name="page" 
      Title="Reservation" Background="#FFFFFFFF">
    <b:Interaction.Triggers>
        <b:EventTrigger EventName="Loaded" >
            <b:InvokeCommandAction Command="{Binding LoadedCommand}" CommandParameter="{Binding ElementName=page}" />
        </b:EventTrigger>
    </b:Interaction.Triggers>
    <Page.Resources>
        <DataTemplate x:Key="Status">
            <c:StatusComboBox Name="cboStatus" SelectedValue="{Binding Status, Mode=TwoWay, UpdateSourceTrigger=Explicit,Converter={StaticResource EnumToIntConverter}}">
                <b:Interaction.Triggers>
                    <b:EventTrigger EventName="SelectionChanged" >
                        <b:InvokeCommandAction Command="{Binding DataContext.SelectionChangedCommand,RelativeSource={RelativeSource AncestorType=Page}}" CommandParameter="{Binding ElementName=cboStatus}"/>
                    </b:EventTrigger>
                </b:Interaction.Triggers>
            </c:StatusComboBox>
        </DataTemplate>
        <DataTemplate x:Key="OperationKey">
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
                <Button Name="btnEdit" Content="編輯" Background="#00FFFFFF" Style="{StaticResource MaterialDesignOutlinedButton}" Command="{Binding DataContext.EditCommand, RelativeSource={RelativeSource AncestorType=Page}}" CommandParameter="{Binding Path=DataContext, ElementName=btnEdit}" />
                <Button Name="btnView" Content="詳情" Margin="10,0,0,0" ToolTip="详情" Command="{Binding DataContext.ViewDetailsCommand, RelativeSource={RelativeSource AncestorType=Page}}" CommandParameter="{Binding Path=DataContext, ElementName=btnView}" />
            </StackPanel>
        </DataTemplate>
    </Page.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="50" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Grid VerticalAlignment="Top">
            <f:FunctionArea AddButtonText="新增預約" />
        </Grid>
        <Grid Name="maskContainer" Row="1">
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition Height="5" />
                <RowDefinition Height="30" />
            </Grid.RowDefinitions>
            <p:PagingDataGrid Name="ucDataGrid" OperatingKey="OperationKey" OperatingWidth="150*" EnableSelectAll="True" PageData="{Binding PageData}" PagingData="{Binding PagingData}" />
        </Grid>
    </Grid>
</Page>

上面代码中<p:PagingDataGrid Name=”ucDataGrid” OperatingKey=”OperationKey” OperatingWidth=”150*” EnableSelectAll=”True” PageData=”{Binding PageData}” PagingData=”{Binding PagingData}” />就是应用动态分页表格的方法,注意绑定数据是你封装的用户控件的PageData依赖属性,而里面绑定的数据源PageData则是ViewModel基类定义的分页数据属性,需要在各子类的ViewModel里面调用接口赋值,如下代码所示:

        /// <summary>
        /// 綁定分頁數據
        /// </summary>
        [WaitComplete]
        protected async override Task<object> BindPagingData()
        {
            var request = this.GetQueryRules(Query);
            var response = await RestService.For<IReservationApi>(AuthClient).GetPageingData(request);
            if (response.Succeeded)
            {
                PageData = response.Data.Rows;
                this.PagingData.Total = response.Data.Total;
            }
            return response;
        }

总结一下

合理的进行WPF用户控件封装,可以减少很多重复的代码,本文阐述了怎样封装用户控件的思想,实际项目中,可以结合团队情况和项目状况来封装,总之身处IT江湖,名正言顺的偷偷懒也是向老板多要工资的理由,因为你干活又快又好,哪个包工头不喜欢呢?

下一篇将介绍一下在团队开发中,关于目录文件遵循的一些原则,如果有更好的方式,也欢迎大家提出来。

本系列相关阅读

  1. WPF企业级开发框架搭建指南(启示录)
  2. JHRS开发框架之基础类库
  3. JHRS开发框架之第三方框架选型
  4. JHRS开发框架之WPF调用Web API封装
  5. JHRS开发框架之客户端入口项目
  6. JHRS开发框架之各子系统如何整合
  7. JHRS开发框架之怎样设计合理的ViewModel基类
  8. JHRS开发框架之公用组件用户控件的封装
  9. JHRS开发框架之建议遵循的一些建目录文件原则
  10. JHRS开发框架之WPF数据验证
  11. JHRS开发框架之ViewModel相互传参和弹框回传参的解决办法
  12. JHRS开发框架之踩坑记(终章)
退出移动版