接續之前的ObservableCollection<string>,在此紀錄動態資料ObservableCollection<INotifyPropertyChanged>的簡單範例,以常見的程式log舉例
上圖是之前字串轉列舉(static Enum ConvertTo(this string obj, Type enumType))範例的log
XAML:
<TabItem Header="AppLog">
<Grid Background="#FFE5E5E5">
<DataGrid x:Name="DataGridAppLog" Margin="0" AutoGenerateColumns="False" CanUserAddRows="False" SelectionMode="Extended" SelectionUnit="Cell" IsSynchronizedWithCurrentItem="True">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding CreatedTime}"/>
<DataGridTextColumn Binding="{Binding Level}"/>
<DataGridTextColumn Binding="{Binding ThreadID}"/>
<DataGridTextColumn Binding="{Binding CallerLineNumber}"/>
<DataGridTextColumn Binding="{Binding CallerMemberName}"/>
<DataGridTextColumn Binding="{Binding Message}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</TabItem>
C#:
[Serializable]
class AppLog : NotifyPropertyChanged
{
//利用屬性名稱(PropertyInfo.Name)建立AppLog的欄位特性(ColumnAttribute)索引
public static readonly Dictionary<string, (ColumnAttribute, PropertyInfo)> PropertyMap = typeof(AppLog).GetColumnAttrMapByProperty<ColumnAttribute>(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.SetProperty);
[Column("建立者", CSVIndex = -1)]
public string Creator { get; set; }
[Column("日期", CSVIndex = -1)]
public DateTime CreatedDate => CreatedTime.Date;
[Column("時間", WPFDisplayIndex = 0, WPFStringFormat = "{0:HH:mm:ss.fff}")]
public DateTime CreatedTime
{
get { return _createdTime; }
set { OnPropertiesChanged(ref _createdTime, value, nameof(CreatedTime), nameof(CreatedDate)); } //多欄位資料異動
}
[Column("等級", WPFDisplayIndex = 1)]
public string Level { get; set; }
[Column("執行緒", "緒", WPFDisplayIndex = 2, WPFHorizontalAlignment = WPFHorizontalAlignment.Right)]
public int ThreadID { get; set; }
[Column("訊息", WPFDisplayIndex = 5)]
public string Message { get; set; }
[Column("原始碼行號", "行", WPFDisplayIndex = 3, WPFHorizontalAlignment = WPFHorizontalAlignment.Right)]
public int CallerLineNumber { get; set; }
[Column("呼叫端方法或屬性名稱", WPFDisplayIndex = 4, WPFForeground = "MediumBlue")]
public string CallerMemberName { get; set; }
public AppLog([CallerMemberName] string memberName = "")
{
Creator = memberName;
CreatedTime = DateTime.Now;
Level = string.Empty;
ThreadID = 0;
Message = string.Empty;
CallerLineNumber = 0;
CallerMemberName = string.Empty;
}
}
//根據資料結構的欄位特性(ColumnAttribute)描述,為DataGrid的欄位(DataGridTextColumn)建立初始設定
MainWindow.DataGridAppLog.SetHeadersByBindings(AppLog.PropertyMap.Values.ToDictionary(x => x.Item2.Name, x => x.Item1));
//建立AppLog動態資料集,取得資料集的檢視物件,與DataGrid建立關聯檢視
ObservableCollection<AppLog> _appLogCollection = MainWindow.DataGridAppLog.SetViewAndGetObservation<AppLog>();
//欄位特性,非正規XAML寫法,將CSV檔和DataGrid的欄位元資料(metadata)設定收攏在程式碼的同一個區塊,有需要也能再擴充,譬如將DataTable和Excel的欄位元資料收攏,日後資料的紀錄與呈現有需要調整時,各種不同的載體能在同一個程式碼區塊內一起調整
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
class ColumnAttribute : Attribute
{
public string CSVName { get; set; }
public int CSVIndex { get; set; }
public string CSVStringFormat { get; set; }
public string WPFName { get; set; }
public int WPFDisplayIndex { get; set; }
public string WPFStringFormat { get; set; }
public bool WPFIsReadOnly { get; set; }
public WPFVisibility WPFVisibility { get; set; }
public bool WPFCanUserReorder { get; set; }
public bool WPFCanUserSort { get; set; }
public WPFHorizontalAlignment WPFHorizontalAlignment { get; set; }
public string WPFForeground { get; set; }
public ColumnAttribute(string csvName, string wpfName)
{
CSVName = csvName;
CSVIndex = 0;
CSVStringFormat = string.Empty;
WPFName = wpfName;
WPFDisplayIndex = -1;
WPFStringFormat = string.Empty;
WPFIsReadOnly = true;
WPFVisibility = WPFVisibility.Visible;
WPFCanUserReorder = true;
WPFCanUserSort = false;
WPFHorizontalAlignment = WPFHorizontalAlignment.Left;
WPFForeground = string.Empty; //"MediumBlue";
}
public ColumnAttribute(string csvName) : this(csvName, csvName)
{
//
}
public ColumnAttribute() : this(string.Empty, string.Empty)
{
//
}
}
//利用屬性名稱(PropertyInfo.Name)建立指定資料結構的欄位特性(ColumnAttribute)索引
static Dictionary<string, (T, PropertyInfo)> GetColumnAttrMapByProperty<T>(this Type obj, BindingFlags flags) where T : ColumnAttribute
{
Dictionary<string, (T, PropertyInfo)> result = new Dictionary<string, (T, PropertyInfo)>();
PropertyInfo[] piArr = obj.GetProperties(flags);
foreach (PropertyInfo pi in piArr)
{
try
{
Attribute attr = Attribute.GetCustomAttribute(pi, typeof(T), false);
if (attr is T column)
{
result.Add(pi.Name, (column, pi));
}
}
catch (ArgumentException ex)
{
throw new ArgumentException($"{pi.Name}|{ex.Message}");
}
catch
{
throw;
}
}
return result;
}
//根據欄位特性(ColumnAttribute),為DataGrid的欄位(DataGridTextColumn)建立初始設定
static void SetHeadersByBindings<T>(this DataGrid obj, IDictionary<string, T> propertyNameMap) where T : ColumnAttribute
{
foreach (DataGridColumn column in obj.Columns)
{
if (column is DataGridBoundColumn bound && bound.Binding is Binding bind)
{
if (propertyNameMap.TryGetValue(bind.Path.Path, out T attr))
{
column.Header = attr.WPFName;
column.DisplayIndex = attr.WPFDisplayIndex;
bind.StringFormat = attr.WPFStringFormat;
column.IsReadOnly = attr.WPFIsReadOnly;
column.Visibility = attr.WPFVisibility.ToString().ConvertTo<Visibility>();
column.CanUserReorder = attr.WPFCanUserReorder;
column.CanUserSort = attr.WPFCanUserSort;
//https://stackoverflow.com/questions/4577944/how-to-resize-wpf-datagrid-to-fit-its-content
column.Width = new DataGridLength(1.0, DataGridLengthUnitType.Auto);
Style headerS = new Style(typeof(DataGridColumnHeader));
headerS.Setters.Add(new Setter(ToolTipService.ToolTipProperty, $"{column.DisplayIndex},{attr.CSVName},{bind.Path.Path},{bind.StringFormat}"));
column.HeaderStyle = headerS;
//https://stackoverflow.com/questions/53961533/datagrid-columns-element-style-in-codebehind-has-no-effect
if (column is DataGridTextColumn col)
{
Style elementS = null;
if (attr.WPFHorizontalAlignment != WPFHorizontalAlignment.Left)
{
elementS = new Style();
elementS.Setters.Add(new Setter(FrameworkElement.HorizontalAlignmentProperty, attr.WPFHorizontalAlignment.ToString().ConvertTo<HorizontalAlignment>()));
}
if (!string.IsNullOrWhiteSpace(attr.WPFForeground))
{
if (elementS == null)
{
elementS = new Style();
}
elementS.Setters.Add(new Setter(TextBlock.FontWeightProperty, FontWeights.DemiBold));
elementS.Setters.Add(new Setter(TextBlock.ForegroundProperty, new BrushConverter().ConvertFromString(attr.WPFForeground)));
}
if (elementS != null)
{
col.ElementStyle = elementS;
}
}
}
}
}
}
以上是很複雜的簡單範例,大部分方法打包成共用程式碼,設計其他資料結構時,只要遵循序列化([Serializable]),繼承NotifyPropertyChanged,建立欄位特性(ColumnAttribute)索引PropertyMap的原則,就能專心在資料結構的設計上,專心在單一程式碼區塊上
其他像是初始化DataGrid的欄位(SetHeadersByBindings),或是建立動態資料集(ObservableCollection<INotifyPropertyChanged>)與DataGrid的關聯檢視,能一行程式碼完成