目录
介绍
为什么我们在这里
使用情况
其他可选的视觉样式
ColWidth属性
ColSort属性
ColCellTemplate属性
另一种样式选项——排序箭头颜色
代码
属性
排序装饰器
AutomaticListView——呈现列
AutomaticListView——排序列
兴趣点
结束语
- 下载AuoGenListView_Example_20210218.zip-675.3 KB
这是我的系列文章中的另一篇,它们是作为真实世界编码的直接结果而产生的,并且反映了对标准WPF ListView的有用变化。我不会讲理论或最佳实践,而是我为特定目的而编写的代码的最终结果,它的通用性足以供其他人使用。因此,系好安全带,享受旅程吧。
我整理了这篇文章,以期减少注意力跨度。首先是强制性的介绍文本,其次是如何使用代码,并以合理详细的代码讨论结束。这应该使每个人都满意,所以我们不会容忍任何抱怨。
本文附带的下载提供了.Net Framework和.Net 5.0(核心)项目集。它们被清楚地标记并且操作相同。由于该解决方案包含两个.Net 5项目,因此必须使用Visual Studio 2019对其进行编译,或者将两个.Net 5项目转换为使用.Net Core 3.1。
在工作中,我最近不得不开发一种工具,该工具使我们能够测试在数据库刷新过程中执行的计算,该过程涉及超过50万名成员。该工具将涉及大约十二个不同的窗口,每个窗口将包含一个ListView,并且每个窗口都绑定到不同的项目结构的集合上。您可能会猜到,每个实体集合通常都需要为每个ListView实体进行大量的列定义工作,而我真的不想处理这个问题。完美的解决方案是一种可以根据ListView实体的内容自动生成其列的解决方案。我一点都不惊讶发现标准的WPF ListView没有开箱即用地提供这种功能。这意味着我必须“自己动手”,这最终并不是一件太可怕的任务。这也让我以可选属性和资源的形式添加了一些自定义项。
使用情况这是最小化实现AutomaticListView的步骤。下面提供的示例代码仅在此处用于说明需要完成的工作,并且是示例代码中内容的缩写。
0)将名称空间条目添加到您的Window/ UserControl XAML文件:
1)将AutomaticListView控件添加到您的Window(或UserControl):
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
{
public VMSampleItemList()
{
this.Add(new VMSampleItem() {...} );
this.Add(new VMSampleItem() {...} );
this.Add(new VMSampleItem() {...} );
this.Add(new VMSampleItem() {...} );
}
}
就是这样。您的ListView将显示所有用ColVisible属性装饰的列。列宽是根据所测量的属性名称(也用作标题文本)的宽度自动计算的。尽管很有趣,但是我们可以做的还很多。
我更喜欢看到我的列表显示为灰色条形,其中每行的背景色都略深(如本文顶部的强制性屏幕截图所示)。为此,可以将以下XAML添加到App.xaml文件或适当的资源字典中。
要启用灰色栏着色,您还需要在Window/ UserControl中为ListView更改XAML(这不仅限于AutomaticListView,并且将在应用程序中的任何ListView中工作)。
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中,它看起来像这样:
在后面的代码中,关联的代码如下所示:
private void GridViewColumnHeader_Click(object sender, RoutedEventArgs e)
{
AutomaticListView lvData = sender as AutomaticListView;
lvData.ProcessSortRequest(e);
}
我喜欢在父容器中处理事件的原因是因为有时我想在排序发生之前或之后进行一些其他处理。
有时由于给定的一组要求,您可能希望为特定的列设置样式。在我的例子中,我想让一个列使用非比例字体,因为数据总是6个字母数字字符宽,而看到它使用比例字体显示会把人们的注意力从数据上吸引开,实际上会让扫描列变得更困难。我的第一步是简单地更改字体系列和大小,但是我突然意识到,“当我在这里的时候”,我可以毫不费力地添加一些更合理的样式功能:
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或适当的资源字典中添加以下资源。如果代码找不到它们,箭头将变成黑色(注意键名——这些是代码要查找的资源名称——并且区分大小写)
代码
您可能会猜到,最有趣的代码是在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 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——呈现列
此类呈现可见的colmns并提供排序功能。渲染列涉及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;
}
辅助方法(创建该方法是为了使main方法中的代码量保持低沉):
///
/// Determine the width of the column, using the largest of either the calculated width,
/// or the decorated width (using ColWidth attribute).
///
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( "",
"",
"",
"",
" ",
"",
"",
"");
///
/// 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;
}
///
/// Calculates the width of the specified text base on the framework element and the
/// specified font family/size.
///
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;
}
///
/// Loads the specified XAML string
///
///
///
private object XamlReaderLoad(string xaml)
{
var xamlObj = XamlReader.Load(new MemoryStream(Encoding.ASCII.GetBytes(xaml)));
return xamlObj;
}
AutomaticListView——排序列
分类的行为完全在AutomaticListView类内完成。排序时有两个主要注意事项,这些注意事项在DetermineSortCriteria方法中处理。我将再次提供带注释的源代码,而不是带有单独叙述的源代码。
///
/// 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.
///
///
/// The displayed header column text
/// Whether or not the column can be sorted
///
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,但是至少您不会从Microsoft获得免费的赠予来。
如果打算重新编译以用于跨平台使用,则必须放弃ColCellTemplateAttribute类中的字体系列验证,但这很容易做到。只需在Visual Studio中打开解决方案,编辑AutoGenListView.NetCore项目的属性,删除__WINDOWS_TARGET__编译器定义(在“生成”选项卡上),然后重新生成解决方案。如果要查看执行此操作时所做的更改,请查看AutoGenListView.NetCore/Attributes/ColCellTemplateAttribute.cs文件。
结束语总的来说,这对我来说是一个非常有用的附带项目。我不得不多次回到实体,并在列表视图中四处移动,调整列大小或添加/删除列。我确实不喜欢在XAML中乱来,并且会采取几乎英勇的措施来避免这种情况。在编写此代码的过程中,使我想起了WPF多么奇怪,以及它在绑定和资源方面的严格程度。当然,缺点是,在我高龄的时候,我可能不得不为我的下一个项目重新学习这些废话。
https://www.codeproject.com/Articles/5295136/Auto-generated-columns-in-a-WPF-ListView