WPF 样式化 RowDefinitions 和 ColumnDefinitions






4.31/5 (8投票s)
使用附加属性和样式设置器增强 WPF 的可皮肤性和资源定义。
引言
我最喜欢 WPF 编程的地方在于,我们可以轻松地将应用程序组件划分开来。当然,每个 Page
或 UserControl
都有一个标记和一个代码隐藏文件,但如果你像我一样认真对待架构模式,那么你可能还有一个 ViewModel 以及可能相关的资源文件。
我更喜欢保持标记文件的整洁,将视觉属性定义在单独的资源文件中。这样做,以后添加或删除元素,或者更改视觉属性,都会更快、更灵活……同时还能带来 S.O.C.(关注点分离)的宁静。
无论你的样式是在本地标记文件中定义的,还是在合并的资源字典中定义的,样式化都存在一些固有的限制,这些限制很容易被克服。以 RowDefinition
和 ColumnDefinition
为例……你可以仔细解析 UserControl
的视觉属性,但最终你必须在标记文件中定义它们,并且除非你在运行时用托管代码修改它,否则这些值将不可避免地是**静态**的。对于可皮肤的界面来说,这不是很友好,对吧?
背景
当我想解决这个问题时,我知道它必须很简单。我首先想到的是一个自定义的 Behavior
,但我很快就放弃了这个想法。相反,我转向了 WPF 中最被低估的元素之一——附加属性。
附加属性是一种特殊的依赖属性,可以分配给任何依赖对象。由于 Grid
恰好是一个依赖对象,所以这个解决方案不仅足够,而且很优雅。我每天都在使用附加属性,并且用于各种目的。我很少不使用它们就构建应用程序!
使用代码
附加属性可以定义在应用程序的**任何**类中。为了演示,本文假设你将声明放在 Application
类中。
public static DependencyProperty GridRowsProperty =
DependencyProperty.RegisterAttached("GridRows", typeof(string),
MethodBase.GetCurrentMethod().DeclaringType, new FrameworkPropertyMetadata(string.Empty,
FrameworkPropertyMetadataOptions.AffectsArrange, new PropertyChangedCallback(GridRowsPropertyChanged)));
Public Shared GridRowsProperty As DependencyProperty =
DependencyProperty.RegisterAttached("GridRows", GetType(String),
MethodBase.GetCurrentMethod().DeclaringType, New FrameworkPropertyMetadata(String.Empty,
FrameworkPropertyMetadataOptions.AffectsArrange, New PropertyChangedCallback(AddressOf GridRowsPropertyChanged)))
我们将附加属性声明为 String
类型,以便在标记中输入逗号分隔的列/行定义列表。
注意:由于我使用了大量的代码片段库,我使用反射来设置依赖属性声明中的 ownerType
。用 GetType(Application)
(或者你声明附加属性的任何类)替换 MethodBase.GetCurrentMethod().DeclaringType
是完全可以的。
为了使附加属性起作用,我们必须定义 Get
和 Set
访问器。如果你不熟悉依赖属性系统,重要的是要注意命名约定非常具体。
public static string GetGridRows(Grid This)
{
return Convert.ToString(This.GetValue(GridRowsProperty));
}
public static void SetGridRows(Grid This, string Value)
{
This.SetValue(GridRowsProperty, Value);
}
Public Shared Function GetGridRows(ByVal This As Grid) As String
Return CType(This.GetValue(GridRowsProperty), String)
End Function
Public Shared Sub SetGridRows(ByVal This As Grid, ByVal Value As String)
This.SetValue(GridRowsProperty, Value)
End Sub
在 GridRowsProperty
声明中,我们分配了 PropertyChangedCallback
方法 GridRowsPropertyChanged
。当 GridRowsProperty
的值被初始化或修改时,将调用此方法。
Private Shared Sub GridRowsPropertyChanged(ByVal Sender As Object, ByVal e As DependencyPropertyChangedEventArgs)
Dim This = TryCast(Sender, Grid)
If This Is Nothing Then Throw New Exception("Only elements of type 'Grid' can utilize the 'GridRows' attached property")
DefineGridRows(This)
End Sub
private static void GridRowsPropertyChanged(object Sender, DependencyPropertyChangedEventArgs e)
{
object This = Sender as Grid;
if (This == null)
throw new Exception("Only elements of type 'Grid' can utilize the 'GridRows' attached property");
DefineGridRows(This);
}
在回调方法中,我们只是检查该属性是否已附加到 Grid
元素,然后调用修改 RowDefinition
的方法。
private static void DefineGridRows(Grid This)
{
object Rows = GetGridRows(This).Split(Convert.ToChar(","));
This.RowDefinitions.Clear();
foreach ( Row in Rows) {
switch (Row.Trim.ToLower) {
case "auto":
This.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Auto) });
break;
case "*":
This.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
break;
default:
if (System.Text.RegularExpressions.Regex.IsMatch(Row, "^\\d+\\*$")) {
This.RowDefinitions.Add(new RowDefinition { Height = new GridLength(
Convert.ToInt32(Row.Substring(0, Row.IndexOf(Convert.ToChar("*")))), GridUnitType.Star) });
} else if (Information.IsNumeric(Row)) {
This.RowDefinitions.Add(new RowDefinition { Height = new GridLength(Convert.ToDouble(Row), GridUnitType.Pixel) });
} else {
throw new Exception("The only acceptable value for the 'GridRows' " +
"attached property is a comma separated list comprised of the following options:" +
Constants.vbCrLf + Constants.vbCrLf + "Auto,*,x (where x is the pixel " +
"height of the row), x* (where x is the row height multiplier)");
}
break;
}
}
}
Private Shared Sub DefineGridRows(ByVal This As Grid)
Dim Rows = GetGridRows(This).Split(CChar(","))
This.RowDefinitions.Clear()
For Each Row In Rows
Select Case Row.Trim.ToLower
Case "auto"
This.RowDefinitions.Add(New RowDefinition With {.Height = New GridLength(1, GridUnitType.Auto)})
Case "*"
This.RowDefinitions.Add(New RowDefinition With {.Height = New GridLength(1, GridUnitType.Star)})
Case Else
If System.Text.RegularExpressions.Regex.IsMatch(Row, "^\d+\*$") Then
This.RowDefinitions.Add(New RowDefinition With {.Height = New _
GridLength(CInt(Row.Substring(0, Row.IndexOf(CChar("*")))), GridUnitType.Star)})
ElseIf IsNumeric(Row) Then
This.RowDefinitions.Add(New RowDefinition With {.Height = New GridLength(CDbl(Row), GridUnitType.Pixel)})
Else
Throw New Exception("The only acceptable value for the 'GridRows' " & _
"attached property is a comma separated list comprised of the following options:" & _
vbCrLf & vbCrLf & "Auto,*,x (where x is the pixel " & _
"height of the row), x* (where x is the row height multiplier)")
End If
End Select
Next
End Sub
DefineGridRows
方法首先调用 GridRowsProperty
的 Get 访问器,它返回一个字符串,该字符串表示逗号分隔的 RowDefinition
值列表。
接下来,我们清除 RowDefinitionCollection
中任何现有的 RowDefinition
。
使用 String
类型允许我们在 XAML 中或通过数据绑定定义无限数量的行/列,但这意味着我们必须解析字符串数据才能确定我们的 RowDefinition
实际是什么。
在评估字符串值之前,我们调用 String
函数 Trim()
和 ToLower()
。这增加了 XAML 定义(或绑定)的灵活性,因为我们不必担心大小写敏感性或空格。
我们的附加属性将支持 4 种(我知道实际上是 3 种,但由于有 4 种实现方式,所以称之为 4 种更有意义)行/列定义类型,详细解释 在此。如上所示,我们遍历提供的值,并根据值添加一个新的 RowDefinition
。
请注意,如果出现解析错误,我们将抛出一个描述性的错误消息,以便在调试过程中更清晰。
代码到此为止!现在只剩下标记了。确保在你的指令中引用了你的程序集
<UserControl x:class="MyUserControl" mc:ignorable="d"
xmlns:local="clr-namespace:MyApplication"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"/>
现在你可以像这样在 XAML 中定义 RowDefinition
<Grid local:Application.GridRows="Auto,50,*,Auto" />
这相当于
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="50"/>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
</Grid>
或者更重要的是,你可以将 RowDefinition
从应用程序中任何位置定义的 Style
中作为资源来定义
<Style x:Key="StylizedRowDefinitions" TargetType="Grid">
<Setter Property="local:Application.GridRows" Value="3*,5*,Auto"/>
</Style>
<Grid Style="{DynamicResource StylizedRowDefinitions}">
</Grid>
现在我们将实现 GridColumnsProperty
,然后我们可以看到所有的代码
Public Shared GridColumnsProperty As DependencyProperty =
DependencyProperty.RegisterAttached("GridColumns", GetType(String),
MethodBase.GetCurrentMethod().DeclaringType, New FrameworkPropertyMetadata(String.Empty,
FrameworkPropertyMetadataOptions.AffectsArrange, New PropertyChangedCallback(AddressOf GridColumnsPropertyChanged)))
Private Shared Sub GridColumnsPropertyChanged(ByVal Sender As Object, ByVal e As DependencyPropertyChangedEventArgs)
RaiseEvent _GridColumnsChanged(Sender, e)
Dim This = TryCast(Sender, Grid)
If This Is Nothing Then
Throw New Exception("Only elements of type 'Grid' " +
"can utilize the 'GridColumns' attached property")
DefineGridColumns(This)
End Sub
Public Shared Function GetGridColumns(ByVal This As Grid) As String
Return CType(This.GetValue(GridColumnsProperty), String)
End Function
Public Shared Sub SetGridColumns(ByVal This As Grid, ByVal Value As String)
This.SetValue(GridColumnsProperty, Value)
End Sub
Private Shared Sub DefineGridColumns(ByVal This As Grid)
Dim Columns = GetGridColumns(This).Split(CChar(","))
This.ColumnDefinitions.Clear()
For Each Column In Columns
Select Case Column.Trim.ToLower
Case "auto"
This.ColumnDefinitions.Add(New ColumnDefinition With {.Width = New GridLength(1, GridUnitType.Auto)})
Case "*"
This.ColumnDefinitions.Add(New ColumnDefinition With {.Width = New GridLength(1, GridUnitType.Star)})
Case Else
If System.Text.RegularExpressions.Regex.IsMatch(Column, "^\d+\*$") Then
This.ColumnDefinitions.Add(New ColumnDefinition With {.Width = _
New GridLength(CInt(Column.Substring(0, _
Column.IndexOf(CChar("*")))), GridUnitType.Star)})
ElseIf IsNumeric(Column) Then
This.ColumnDefinitions.Add(New ColumnDefinition With _
{.Width = New GridLength(CDbl(Column), GridUnitType.Pixel)})
Else
Throw New Exception("The only acceptable value for " &_
"the 'GridColumns' attached property is a comma separated " & _
"list comprised of the following options:" & vbCrLf & _
vbCrLf & "Auto,*,x (where x is the pixel width of the " & _
"column), x* (where x is the column width multiplier)")
End If
End Select
Next
End Sub
public static DependencyProperty GridColumnsProperty =
DependencyProperty.RegisterAttached("GridColumns", typeof(string),
MethodBase.GetCurrentMethod().DeclaringType, new FrameworkPropertyMetadata(string.Empty,
FrameworkPropertyMetadataOptions.AffectsArrange, new PropertyChangedCallback(GridColumnsPropertyChanged)));
private static void GridColumnsPropertyChanged(object Sender, DependencyPropertyChangedEventArgs e)
{
if (_GridColumnsChanged != null) {
_GridColumnsChanged(Sender, e);
}
object This = Sender as Grid;
if (This == null)
throw new Exception("Only elements of type 'Grid' can " +
"utilize the 'GridColumns' attached property");
DefineGridColumns(This);
}
public static string GetGridColumns(Grid This)
{
return Convert.ToString(This.GetValue(GridColumnsProperty));
}
public static void SetGridColumns(Grid This, string Value)
{
This.SetValue(GridColumnsProperty, Value);
}
private static void DefineGridColumns(Grid This)
{
object Columns = GetGridColumns(This).Split(Convert.ToChar(","));
This.ColumnDefinitions.Clear();
foreach ( Column in Columns) {
switch (Column.Trim.ToLower) {
case "auto":
This.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) });
break;
case "*":
This.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
break;
default:
if (System.Text.RegularExpressions.Regex.IsMatch(Column, "^\\d+\\*$")) {
This.ColumnDefinitions.Add(new ColumnDefinition { Width =
new GridLength(Convert.ToInt32(Column.Substring(0,
Column.IndexOf(Convert.ToChar("*")))), GridUnitType.Star) });
} else if (Information.IsNumeric(Column)) {
This.ColumnDefinitions.Add(new ColumnDefinition {
Width = new GridLength(Convert.ToDouble(Column), GridUnitType.Pixel) });
} else {
throw new Exception("The only acceptable value for the 'GridColumns' attached " +
"property is a comma separated list comprised of the following options:" +
Constants.vbCrLf + Constants.vbCrLf +
"Auto,*,x (where x is the pixel width of the column), " +
"x* (where x is the column width multiplier)");
}
break;
}
}
}
现在定义你的列定义就像定义你的 RowDefinition
一样简单
<Style x:Key="MyGridStyle" TargetType="Grid">
<Setter Property="local:Application.GridColumns" Value="Auto,*"/>
<Setter Property="local:Application.GridRows" Value="1*,*,Auto"/>
<Style>
<Grid Style="{DynamicResource MyGridStyle}">
<Grid>
关注点
样式化行/列定义弥合了 WPF 可伸缩性中存在的差距,并有助于使你的应用程序更具动态性。与许多其他方法相比,附加属性是一种实现可伸缩性的被大大低估的方法,而且通常代码量很少。
历史
- 提交日期:2012/7/24。
- 添加了 C# 示例:2012/7/24。