65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF ListView 中的自动生成列

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2021年2月18日

CPOL

10分钟阅读

viewsIcon

13006

downloadIcon

319

一个 WPF ListView, 它根据装饰的实体属性自动生成列(这些列也可以排序)

引言

这是我系列文章中的又一篇,直接源于实际编码,并展示了标准 WPF ListView 的一个有用变体。我不会在这里探讨理论或最佳实践,而是分享我为实现特定目的而编写的代码的最终结果,这些代码足够通用,可以被其他人使用。所以,请坐稳,享受这段旅程。

我组织了这篇文章,以适应注意力广度递减的读者。首先是必要的介绍文本,然后是代码使用方法,最后是对代码的相当详细的讨论。这应该能满足几乎所有人,所以不会容忍任何抱怨。

本文附带的下载内容同时提供了 .Net Framework 和 .Net 5.0 (Core) 项目集。它们有清晰的标记,并且运行方式相同。由于解决方案包含两个 .Net 5 项目,因此您必须使用 Visual Studio 2019 才能编译它,或者将这两个 .Net 5 项目转换为使用 .Net Core 3.1。

AutomaticListView example apps
 

我们为什么在这里

在我工作的地方,我最近不得不开发一个工具,用于测试在涉及超过五十万成员的数据库刷新过程中执行的计算。这个工具将包含大约十几个不同的窗口,每个窗口都将包含一个 ListView,并且每个 ListView 都将绑定到结构迥异的项目集合。正如您可能猜到的,每个实体集合通常都需要为每个 ListView 进行广泛的列定义工作,而我真的不想处理这些。理想的解决方案是创建一个 ListView,该 ListView 可以根据实体的内容自动生成其列。我一点也不惊讶地发现,标准的 WPF ListView 默认不提供这种功能。这意味着我必须“自己动手”,结果证明这并非一项过于艰巨的任务。我还因此添加了一些自定义功能,例如可选的属性和资源。

用法

以下是实现 AutomaticListView 的最小步骤。下方提供的示例代码仅用于说明需要执行的操作,并且是示例代码的简化版本。

0)在您的 Window/UserControl XAML 文件中添加命名空间条目
<Window x:Class="AuoGenListView_Example.MainWindow"
		...
		xmlns:aglv="clr-namespace:AutoGenListView;assembly=AutoGenListView"
		...>
1)将 AutomaticListView 控件添加到您的 Window(或 UserControl)中
<Grid >
    <aglv:AutomaticListView x:Name="lvData" SelectionMode="Single" 
                            ItemsSource="{Binding Path=SampleData}" />
</Grid>
2)创建您的 ViewModel 集合实体
public class VMSampleItem : Notifiable
{
    [ColVisible]
    public int    Identity { get; set; }
    [ColVisible]
    public int    Ident2   { get; set; }
    [ColVisible]
    public string Name     { get; set; }
    [ColVisible]
    public string Address  { get; set; }
    [ColVisible]
    public string Info     { get; set; }

    // This property won't be displayed because it's not decorated 
    // with the ColVisible attribute
    public string MoreInfo { get; set; }
}

public class VMSampleItemList : ObservableCollection<VMSampleItem>
{
    public VMSampleItemList()
    {
        this.Add(new VMSampleItem() {...} );
        this.Add(new VMSampleItem() {...} );
        this.Add(new VMSampleItem() {...} );
        this.Add(new VMSampleItem() {...} );
    }
}

基本上就是这样。您的 ListView 将显示所有带有 ColVisible 属性的列。列宽是根据属性名称(也用作标题文本)的测量宽度自动计算的。虽然这很有趣,但我们还有很多事情可以做。

其他可选的视觉样式

我更喜欢看到我的列表带有灰色条纹外观,即每隔一行有一个稍深一点的背景颜色(在本篇文章顶部的截图中有说明)。为了实现这一点,您可以将以下 XAML 添加到您的 App.xaml 文件或适当的资源字典中。

<!-- this style allows us to see gray-bar rows (alternating white/gray rows 
in the <code>ListView</code>), as well as make background changes for specific columns use 
the entire width of the column. -->
<Style TargetType="{x:Type ListViewItem}">
    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
    <!-- the three following properties reduce the gap between rows-->
    <Setter Property="Margin" Value="0,-1,0,0" />
    <Setter Property="Padding" Value="0" />
    <Setter Property="BorderThickness" Value="0" />
    <!--  triggers for gray bar, selected, and mouse hover-->
    <Style.Triggers>
        <Trigger Property="ItemsControl.AlternationIndex" Value="0">
            <Setter Property="Background" Value="White" />
        </Trigger>
        <Trigger Property="ItemsControl.AlternationIndex" Value="1">
            <Setter Property="Background" Value="#eeeeee" />
        </Trigger>
        <Trigger Property="IsSelected" Value="True">
            <Setter Property="Background" Value="LightSkyBlue" />
        </Trigger>
        <Trigger Property="IsMouseOver" Value="True">
            <Setter Property="Background" Value="LightBlue" />
        </Trigger>
    </Style.Triggers>
</Style>

要启用灰色条纹着色,您还需要更改 Window/UserControlListView 的 XAML(这不限于 AutomaticListView,在您应用中的任何 ListView 中都可以工作)。

<aglv:AutomaticListView x:Name="lvData" SelectionMode="Single"  Margin="0,3,0,0" 
                        AlternationCount="2"
                        ItemsSource="{Binding Path=SampleData}"/>

ColWidth 属性

虽然自动计算列宽很方便,但在现实世界中用处不大,因为列中的数据通常比列标题文本(宽得多)。如果您有这样的数据,可以使用 ColWidth 属性来指定所需的列宽。列宽仍然根据测量到的标题文本宽度进行计算,但实际使用的宽度将是计算宽度和指定宽度中的较大者。如下所示,示例应用程序中的几列都指定了列宽。

public class VMSampleItem : Notifiable
{
    [ColVisible]
    public int    Identity { get; set; }
    [ColVisible]
    public int    Ident2   { get; set; }
    [ColVisible][ColWidth(200)]
    public string Name     { get; set; }
    [ColVisible][ColWidth(200)]
    public string Address  { get; set; }
    [ColVisible][ColWidth(325)]
    public string Info     { get; set; }
    public string MoreInfo { get; set; }
}

ColSort 属性

默认情况下,所有列都可以排序,但有时您可能不希望/不需要一个或多个列可排序。如果您有符合此描述的列,可以使用 ColSort 属性来阻止排序

public class VMSampleItem : Notifiable
{
    [ColVisible]
    public int    Identity { get; set; }
    [ColVisible]
    public int    Ident2   { get; set; }
    [ColVisible][ColWidth(200)]
    public string Name     { get; set; }
    [ColVisible][ColWidth(200)]
    public string Address  { get; set; }
    [ColVisible][ColWidth(325)][ColSort(false)]
    public string Info     { get; set; }
    public string MoreInfo { get; set; }
}

当然,设置 ColSort(true) 将允许列排序,但由于排序会自动为每列启用,因此这是不必要的,仅出于完整性的考虑而包含。

要实际启用排序,您必须为单击列标题添加一个事件处理程序。在 XAML 中,它看起来是这样的

<aglv:AutomaticListView x:Name="lvData" SelectionMode="Single"  Margin="0,3,0,0" 
                        AlternationCount="2"
                        ItemsSource="{Binding Path=SampleData}"
                        GridViewColumnHeader.Click="GridViewColumnHeader_Click" />

在代码隐藏中,关联的代码如下所示

private void GridViewColumnHeader_Click(object sender, RoutedEventArgs e)
{
    AutomaticListView lvData = sender as AutomaticListView;
    lvData.ProcessSortRequest(e);
}

我之所以倾向于在父容器中处理事件,是因为有时我想在排序发生之前或之后进行一些额外的处理。

ColCellTemplate 属性

有时,您可能希望根据给定的一组要求对特定列进行样式设置。在我这里,我想让某一列使用非比例字体,因为数据总是由六个字母数字字符组成,用比例字体显示会使眼睛从数据上移开,实际上更难扫描该列。我最初的想法是简单地允许更改字体系列和大小,但我想到了“既然我在做这件事”,我可以毫不费力地添加一些更合理的样式功能

public class VMSampleItem : Notifiable
{
    [ColVisible]
    public int    Identity { get; set; }
    [ColVisible][DisplayName("2nd Identity")]
    public int    Ident2   { get; set; }
    [ColVisible][ColWidth(200)]
    public string Name     { get; set; }
    [ColVisible][ColWidth(200)]
    public string Address  { get; set; }
    [ColVisible][ColWidth(325)][ColSort(false)]
    [ColCellTemplate("Consolas", "18", "Italic", "Black", "Red", "Yellow", "Right", null)]
    public string Info     { get; set; }
    public string MoreInfo { get; set; }
}

您可以指定字体系列、大小、样式和粗细,以及前景色和背景色,以及水平和垂直对齐方式。

另一个样式选项 - 排序箭头颜色

这个选项非常可选,几乎有点好笑。我希望排序箭头不仅仅是普通的黑色,所以我想出了一种轻松改变单个向上/向下箭头颜色的方法。您只需要将以下资源添加到您的 App.xaml 或适当的资源字典中。如果代码找不到它们,箭头将是黑色的(请注意键名 - 这些是代码查找的资源名称 - 并且它们是区分大小写的

<!-- Brush defintions for the sort adorner arrows - if you don't specify 
these, the arrows default to Black. -->
<SolidColorBrush x:Key="SortArrowUpColor" Color="Red" />
<SolidColorBrush x:Key="SortArrowDownColor" Color="Blue" />

代码

正如您可能猜到的,最有趣的代码位于 DLL 中,其中实现了 AutromaticListView、排序修饰符和属性。

属性

以下属性通常不那么重要,只是为了完整性而包含。

//=================================================
[AttributeUsage(AttributeTargets.Property, AllowMultiple=false, Inherited=true)]
public class ColSortAttribute : Attribute
{
    public bool Sort { get; set; }
    public ColSortAttribute()
    {
        Sort = true;
    }
    public ColSortAttribute(bool sort)
    {
        Sort = sort;
    }
}

//=================================================
[AttributeUsage(AttributeTargets.Property, AllowMultiple=false, Inherited=true)]
public class ColVisibleAttribute : Attribute
{
    public bool Visible { get; set; }
    public ColVisibleAttribute()
    {
        this.Visible = true;
    }
    public ColVisibleAttribute(bool visible)
    {
        this.Visible = visible;
    }
}

//=================================================
[AttributeUsage(AttributeTargets.Property, AllowMultiple=false, Inherited=true)]
public class ColWidthAttribute : Attribute
{
    public double Width { get; set; }
    public ColWidthAttribute()
    {
        this.Width = 150;
    }
    public ColWidthAttribute(double width)
    {
        this.Width = Math.Max(0, width);
    }
}

AutomaticListView 还识别 DataAnnotations.DisplayName 属性,因此您可以更改显示列的标题文本。我曾经尝试编写自己的属性类,以便 a) 所有属性类的名称都遵循相同的命名约定,以及 b) 我不必引用另一个 .Net 程序集,但我变得懒惰和厌倦,就这样吧。

重头戏是 ColCellTemplateAttribute 类。由于其属性的数量和类型,更重要的是,因为一些程序员在编写代码时根本不使用常识,我觉得有必要进行验证,以防止出现问题。

首先,我想定义一些枚举,以便更容易地编写验证代码

private enum FieldID              { Name, Size, Style, Weight, Foreground, 
                                    Background, HorzAlign, VertAlign}
private enum AttribHorzAlignments { Center, Left, Right, Stretch }
private enum AttribVertAlignments { Bottom, Center, Stretch, Top }
private enum AttribFontWeights    { Black, Bold, DemiBold, 
                                    ExtraBlack, ExtraBold, ExtraLight, 
                                    Heavy, Light, Medium, Normal, 
                                    Regular, SemiBold, Thin, 
                                    UltraBlack, UltraBold, UltraLight }
private enum AttribFontStyles     { Italic, Normal, Oblique }

接下来,我想创建一个已安装字体集合。它是一个静态字段,因为我不想为该属性的每个实例分配空间。

private static List<string> installedFonts = null;

为了使验证代码更轻松(打字更少),我创建了一个字符串常量,用于在使用 string.Format() 调用时作为基础

private const string _DEFAULT_FORMAT_ = "{{Binding RelativeSource={{RelativeSource FindAncestor,AncestorType=ListViewItem}}, Path={0}}}";

然后是属性。我将所有内容定义为字符串,以便可以在 AutomaticListView 类中轻松使用它们。

public string  FontName   { get; set; }
public string  FontSize   { get; set; }
public string  FontStyle  { get; set; }
public string  FontWeight { get; set; }
public string  Foreground { get; set; }
public string  Background { get; set; }
public string  HorzAlign  { get; set; }
public string  VertAlign  { get; set; }

所有其他属性都提供一个不接受任何参数的默认构造函数。这允许您使用匿名属性来设置值。您也可以使用接受要设置的值的标准化构造函数来设置属性。对于 ColCellTemplate 属性,这并不真正实用,因为我想确保程序员不会做任何愚蠢的事情。

public ColCellTemplateAttribute(string name, string size, string style, string weight, 
                                string fore, string back, string horz,  string vert)
{
    this.FontName   = (!string.IsNullOrEmpty(name))   ? this.Validate(FieldID.Name,       name  ) 
                                                      : string.Format(_DEFAULT_FORMAT_, "FontFamily");
    this.FontSize   = (!string.IsNullOrEmpty(size))   ? this.Validate(FieldID.Size,       size  ) 
                                                      : string.Format(_DEFAULT_FORMAT_, "FontSize");
    this.FontStyle  = (!string.IsNullOrEmpty(style))  ? this.Validate(FieldID.Style,      style ) 
                                                      : string.Format(_DEFAULT_FORMAT_, "FontStyle");
    this.FontWeight = (!string.IsNullOrEmpty(weight)) ? this.Validate(FieldID.Weight,     weight) 
                                                      : string.Format(_DEFAULT_FORMAT_, "FontWeight");
    this.Foreground = (!string.IsNullOrEmpty(fore))   ? this.Validate(FieldID.Foreground, fore  ) 
                                                      : string.Format(_DEFAULT_FORMAT_, "Foreground");
    this.Background = (!string.IsNullOrEmpty(back))   ? this.Validate(FieldID.Background, back  ) 
                                                      : string.Format(_DEFAULT_FORMAT_, "Background");
    this.HorzAlign  = (!string.IsNullOrEmpty(horz))   ? this.Validate(FieldID.HorzAlign,  horz  ) 
                                                      : "Left";
    this.VertAlign  = (!string.IsNullOrEmpty(vert))   ? this.Validate(FieldID.VertAlign,  vert  ) 
                                                      : "Center";
}

验证方法使用 case 语句来决定它正在验证什么。本质上,该方法试图理解程序员指定的意图,如果出现问题,则使用默认设置。这意味着不会有中断 ListView 使用的明显失败迹象,除了指定列的外观可能与您预期的不符。

private string Validate (FieldID id, string value)
{
    string result = value;
    switch (id)
    {
        case FieldID.Background :
            {
                try
                {
                    Color color = (Color)(ColorConverter.ConvertFromString(value));
                    result = value;
                }
                catch(Exception)
                { 
                    result = string.Format(_DEFAULT_FORMAT_, "Background");
                }
            }
            break;
        case FieldID.Foreground :
            {
                try
                {
                    Color color = (Color)(System.Windows.Media.ColorConverter.ConvertFromString(value));
                    result = value;
                }
                catch(Exception)
                { 
                    result = string.Format(_DEFAULT_FORMAT_, "Foreground");
                }
            }
            break;
        case FieldID.HorzAlign :
            {
                AttribHorzAlignments align;
                if (!Enum.TryParse(value, out align))
                {
                    result = AttribHorzAlignments.Left.ToString();
                }
            }
            break;
        case FieldID.Name :
            {
                if (installedFonts == null)
                {
                    using (InstalledFontCollection fontsCollection = new InstalledFontCollection())
                    {
                        installedFonts = (from x in fontsCollection.Families select x.Name).ToList();
                    }
                }
                if (!installedFonts.Contains(value))
                {
                    result = string.Format(_DEFAULT_FORMAT_, "FontFamily");
                }
            }
            break;
        case FieldID.Size :
            {
                double dbl;
                if (!double.TryParse(value, out dbl))
                {
                    result = string.Format(_DEFAULT_FORMAT_, "FontSize");
                }
            }
            break;
        case FieldID.Style :
            {
                AttribFontWeights enumVal;
                if (!Enum.TryParse(value, out enumVal))
                {
                    result = string.Format(_DEFAULT_FORMAT_, "FontStyle");
                }
            }
            break;
        case FieldID.VertAlign :
            {
                AttribVertAlignments align;
                if (!Enum.TryParse(value, out align))
                {
                    result = AttribVertAlignments.Center.ToString();
                }
            }
            break;
        case FieldID.Weight :
            {
                AttribFontWeights weight;
                if (!Enum.TryParse(value, out weight))
                {
                    result = string.Format(_DEFAULT_FORMAT_, "FontWeight");
                }
            }
            break;
    }
    return result;
}

排序修饰符

SortAdorner 类负责绘制适当的箭头以指示排序方向。它唯一可配置的部分是箭头的颜色。默认情况下,向上和向下箭头都是黑色的,但如果找到了预期的画笔资源,颜色将反映在发现的资源中设置的颜色。属性是静态的,所以我们不必为类的每个实例设置它们,并且我们在箭头实际渲染时查找资源。再次,请注意资源的(区分大小写的)名称。

public class SortAdorner : Adorner
{
    private static SolidColorBrush UpArrowColor   { get; set; }
    private static SolidColorBrush DownArrowColor { get; set; }
    ...

    protected override void OnRender(DrawingContext drawingContext)
    {
        // see if we have specially defined arrow colors
        if (UpArrowColor == null)
        {
            // Use TryFindResource instead of FindResource to avoid exception being thrown if the 
            // resources weren't found
            SolidColorBrush up = (SolidColorBrush)TryFindResource("SortArrowUpColor");
            UpArrowColor = (up != null) ? up : Brushes.Black;
            SolidColorBrush down = (SolidColorBrush)TryFindResource("SortArrowDownColor");
            DownArrowColor = (down != null) ? down : Brushes.Black;
        } 
        ...
    }
}

AutomaticListView - 渲染列

此类负责渲染可见列并提供排序功能。渲染列涉及 RenderColumns 方法以及一些辅助方法。我决定不提供代码和旁白,而是提供带有注释的代码。

// this method renders the columns
protected void CreateColumns(AutomaticListView lv)
{
    // get the collection item type
    Type dataType = lv.ItemsSource.GetType().GetMethod("get_Item").ReturnType;
    // create the gridview in which we will populate the columns
    GridView gridView = new GridView() 
    { 
        AllowsColumnReorder = true 
    };

    // get all of the properties that are decorated with the ColVisible attribute
    PropertyInfo[] properties = dataType.GetProperties()
                                        .Where(x=>x.GetCustomAttributes(true)
                                        .FirstOrDefault(y=>y is ColVisibleAttribute) != null)
                                        .ToArray();
    // For each appropriately decorated property in the item "type", make a column 
    // and bind the property to it
    foreach(PropertyInfo info in properties)
    {
        // If the property is being renamed with the DisplayName attribute, 
        // use the new name. Otherwise use the property's actual name
        DisplayNameAttribute dna = (DisplayNameAttribute)(info.GetCustomAttributes(true).FirstOrDefault(x=>x is DisplayNameAttribute));
        string displayName = (dna == null) ? info.Name : dna.DisplayName;

        // Build the cell template if necessary
        DataTemplate cellTemplate = this.BuildCellTemplateFromAttribute(info);

        // determine the column width
        double width = this.GetWidthFromAttribute(info, displayName);

        // if the cellTemplate is null, create a typical binding object for display 
        // member binding
        Binding binding = (cellTemplate != null) ? null : new Binding() { Path = new PropertyPath(info.Name), Mode = BindingMode.OneWay };

        // Create the column, and add it to the gridview. In WPF, you can only specify 
        // binding in one of two places, either the DisplayMemberBinding, or the 
        // CellTemplate. Whichever is used, the other must be null. By the time we get 
        // here, that decision tree has already been processed, and just ONE of the 
        // two binding methods will not be null.
        GridViewColumn column  = new GridViewColumn() 
        { 
            Header               = displayName, 
            DisplayMemberBinding = binding,
            CellTemplate         = cellTemplate,
            Width                = width,
        };
        gridView.Columns.Add(column);
    }
    // set the list view's gridview
    lv.View = gridView;
}

辅助方法(创建目的是为了让主方法中的代码量保持在一个合理的范围内)

/// <summary>
/// Determine the width of the column, using the largest of either the calculated width, 
/// or the decorated width (using ColWidth attribute).
/// </summary>
private double GetWidthFromAttribute(PropertyInfo property, string displayName)
{
    // Get the decorated width (if specified)
    ColWidthAttribute widthAttrib = (ColWidthAttribute)(property.GetCustomAttributes(true).FirstOrDefault(x=>x is ColWidthAttribute));
    double width = (widthAttrib != null) ? widthAttrib.Width : 0d;
    // calc the actual width, and use the larger of the decorated/calculated widths
    width = Math.Max(this.CalcTextWidth(this, displayName, this.FontFamily, this.FontSize)+35, width);
    return width;
}

// This string represents the actual template xaml that will be populated with 
// properties from the specified ColCellTemplates attribute. 
private readonly string _CELL_TEMPLATE_ = string.Concat( "<DataTemplate xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" ",
															"xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" ",
															"x:Key=\"nonPropTemplate\">",
															"<Grid>",
															"<Rectangle Fill=\"{6}\" Opacity=\"0.6\"/>",
															"<TextBlock ",
															"FontFamily=\"{1}\" ",
															"FontSize=\"{2}\" ",
															"FontWeight=\"{3}\" ",
															"FontStyle=\"{4}\" ",
															"Foreground=\"{5}\" ",
															"HorizontalAlignment=\"{7}\" ",
															"VerticalAlignment=\"{8}\" ",
															">",
															"<TextBlock.Text><Binding Path=\"{0}\" Mode=\"OneWay\" /></TextBlock.Text> ",
															"</TextBlock>",
															"</Grid>",
															"</DataTemplate>");


/// <summary>
/// Build the CellTemplate based on the specified ColCelltemplate attribute.
private DataTemplate BuildCellTemplateFromAttribute(PropertyInfo property)
{
    // the attriubte is validated when it's defined, so we don't have to worry about it 
    // by the time we get here. Or do we?
    DataTemplate cellTemplate = null;
    ColCellTemplateAttribute cell = (ColCellTemplateAttribute)(property.GetCustomAttributes(true).FirstOrDefault(x=>x is ColCellTemplateAttribute));
    if (cell != null)
    {
        string xaml = string.Format(_CELL_TEMPLATE_, property.Name, 
                                    cell.FontName, cell.FontSize, cell.FontWeight, cell.FontStyle, 
                                    cell.Foreground, cell.Background,
                                    cell.HorzAlign, cell.VertAlign);
        cellTemplate = (DataTemplate)this.XamlReaderLoad(xaml);
    }
    return cellTemplate;
}

/// <summary>
///  Calculates the width of the specified text base on the framework element and the 
///  specified font family/size.
/// </summary>
private double CalcTextWidth(FrameworkElement fe, string text, FontFamily family, double fontSize)
{
    FormattedText formattedText = new FormattedText(text, 
                                                    CultureInfo.CurrentUICulture, 
                                                    FlowDirection.LeftToRight, 
                                                    new Typeface(family.Source), 
                                                    fontSize,
                                                    Brushes.Black, 
                                                    VisualTreeHelper.GetDpi(fe).PixelsPerDip);
                
    return formattedText.WidthIncludingTrailingWhitespace;
}

/// <summary>
/// Loads the specified XAML string
/// </summary>
/// <param name="xaml"></param>
/// <returns></returns>
private object XamlReaderLoad(string xaml)
{
    var xamlObj = XamlReader.Load(new MemoryStream(Encoding.ASCII.GetBytes(xaml)));
    return xamlObj;
}

AutomaticListView - 排序列

排序操作完全包含在 AutomaticListView 类中。排序时需要考虑两个主要问题,这些问题在 DetermineSortCriteria 方法中进行处理。同样,我将提供带注释的源代码,而不是带有单独旁白的源代码。

/// <summary>
/// Determines if the "sortBy" property name is renamed, and if so, gets the actual 
/// property name that was decorated with the DisplayName attribute. Called by the 
/// ListViewSortColumn() method in this class.
/// </summary>
/// <param name="lv"></param>
/// <param name="sortBy">The displayed header column text</param>
/// <param name="canSort">Whether or not the column can be sorted</param>
/// <returns></returns>
private void DetermineSortCriteria(ref string sortBy, ref bool canSort)
{
    // Determine the item type represented by the collection
    Type collectionType = this.GetBoundCollectionItemType();
    Type[] genericArgs = collectionType.GetGenericArguments();
    if (genericArgs != null && genericArgs.Count() > 0)
    {
        // this is the type of item in the collection
        Type           itemType   = genericArgs.First();

        // find the specified property name
        PropertyInfo   property   = itemType.GetProperty(sortBy);

        // if the property wasn't found, it was probably renamed
        if (property == null)
        {
            // get all of the properties that are visible 
            PropertyInfo[] properties = itemType.GetProperties()
                                                .Where(x=>x.GetCustomAttributes(true)
                                                .FirstOrDefault(y=>y is ColVisibleAttribute) != null)
                                                .ToArray();
           foreach(PropertyInfo prop in properties)
            {
                var dna = (DisplayNameAttribute)(prop.GetCustomAttributes(true).FirstOrDefault(x=>x is DisplayNameAttribute));

                // if the column is renamed
                if (dna != null)
                {
                    // change the sortby value
                    sortBy = (dna.DisplayName == sortBy) ? prop.Name : sortBy;

                    // and set the property
                    property = itemType.GetProperty(sortBy);
                }
            }
        }
        if (property != null)
        {
            var csa = (ColSortAttribute)(property.GetCustomAttributes(true).FirstOrDefault(x=>x is ColSortAttribute));
            canSort = (csa == null || csa.Sort);
        }
        else
        {
            // looks like we can't sort
            canSort = false;
        }
    }
}

关注点

对于 .Net Core 版本,我不得不添加一个 nuget 包 - Microsoft.Windows.Compatibility - 才能访问系统上安装的字体。当我编译代码时,我得到了 642 个文件,52 个文件夹,占用了 170MB 的磁盘空间。这太荒谬了。没有办法选择要包含在编译后的应用程序中的程序集 - 您只会得到全部。

因此,我卸载了该包,并安装了 System.Drawing.Common,所有东西都可以正常编译,而且不会带来一堆文件。这仍然使 DLL 成为 Windows 特定,但至少您不会免费获得微软提供的巨大占用空间。

如果您打算重新编译以用于跨平台使用,则必须放弃 ColCellTemplateAttribute 类中的字体系列验证,但这很容易做到。只需在 Visual Studio 中打开解决方案,编辑 AutoGenListView.NetCore 项目的属性,删除 __WINDOWS_TARGET__ 编译器定义(在“生成”选项卡上),然后重新生成解决方案。如果您想查看执行此操作时所做的更改,请查看 AutoGenListView.NetCore/Attributes/ColCellTemplateAttribute.cs 文件。

结语

总的来说,这对我来说是一个相当有用的副项目。我不得不反复回到实体并重新排列东西,调整列大小,或者在列表视图中添加/删除列。我真的不太喜欢在 XAML 中乱搞,并且会付出近乎英雄般的努力来避免这样做。在编写这段代码的过程中,我再次体会到了 WPF 的奇特之处,以及它在绑定和资源方面的严格性。当然,缺点是,以我这个年纪,我可能不得不在下一个项目中重新学习这些破烂。

历史

  • 2021.02.19 - 修复了一些拼写和标记上的小问题。
     
  • 2021.02.18 - 初次发布。这篇文章可能充斥着拼写错误,但时间已经很晚了,如果需要,我以后会编辑。
     
© . All rights reserved.