wpf 自定義Grid RowDefinitions and ColumnDefinitions 賦*值

Introduction

My favorite part of programming in WPF is the ease with which we compartmentalize the application components. Every Page or UserControl of course has a markup and a code-behind file, but if you are serious about architectural patterns like I am, then you also have a ViewModel and probably a related resource file as well.

I prefer to keep my markup files clean, defining visual attributes in a separate resource file. In doing so, adding or removing elements later, or altering visual properties is done more quickly and with more flexibility...all while providing the serenity that comes with S.O.C. (Separation of concerns).

Whether your styles are defined in the local file markup or a merged resource dictionary, there are some inherent limitations to stylization that can quite easily be overcome. Take for instance RowDefinitions andColumnDefinitions...you can be diligent about parsing out the visual attributes of your UserControl, but ultimately you are forced to define these in your markup file, and unless you modify it in managed code at runtime, these values are inevitably STATIC. Not very frindly to a skinnable interface, is it?

Background

When I decided that this was a problem that I wanted to resolve, I knew that it had to be something simple. My first thought was a custom Behavior, but I quickly got away from that idea. Instead, I went to one of the most underrated elements in WPF - the Attached Property.

Attached Property is a special type of Dependency Property that can be assigned to any Dependency Object. Since a Grid just happens to be a Dependency Object, the solution was not only sufficient, it was elegant. I use Attached Properties daily, and for a wide variety of purposes. I rarely build an app without them!

Using the code

Attached Properties can be defined within ANY class in your application. For demonstration purposes, this article will assume that you placed the declaration in the Application Class.

C#:

 Collapse | Copy Code

public static DependencyProperty GridRowsProperty = 
  DependencyProperty.RegisterAttached("GridRows", typeof(string), 
  MethodBase.GetCurrentMethod().DeclaringType, new FrameworkPropertyMetadata(string.Empty, 
  FrameworkPropertyMetadataOptions.AffectsArrange, new PropertyChangedCallback(GridRowsPropertyChanged)));

We declare the Attached Property as a String type, so that in markup we can enter a comma separated list of Column/Row definitions.

Note: Because I use an extensive library of code snippets, I use Reflection to set the ownerType in my Dependency Property declarations. It is perfectly fine to replace MethodBase.GetCurrentMethod().DeclaringType with GetType(Application) (Or whatever class you are declaring the Attached property within).

In order for an Attached Property to function, we must define the Get and Set Accessors. If you are new to the Dependency Property system, it is important to note that naming convention is very specific.

C#:

public static string GetGridRows(Grid This)
{
    return Convert.ToString(This.GetValue(GridRowsProperty));
}

public static void SetGridRows(Grid This, string Value)
{
    This.SetValue(GridRowsProperty, Value);
}

In the GridRowsProperty declaration, we assigned the PropertyChangedCallback methodGridRowsPropertyChanged. This method will be called when the value of the GridRowsProperty is initialized or modified.

C#:

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);
}

In the callback method, we are simply checking that the property was attached to a Grid element, and then calling the method that modifies the RowDefinitions.

C#:

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;
        }
    }
}

The DefineGridRows method first calls the Get Accessor for the GridRowsProperty, which returns the string that represents a comma separated list of RowDefinition values.

Next we clear any existing RowDefinitions from the RowDefinitionCollection.

Using a String type allows us to define an indefinite number of Rows/Columns in XAML or through databinding, but it means we have to parse the string data to determine what our RowDefinitions will actually be.

Before we evaluate the String value, we call the String Functions Trim() and ToLower(). This adds to the flexibility of the XAML definition (or Binding) because we need not worry about case sensitivity or white space.

Our attached property will support the 4 (I know it's literally 3, but since there's really 4 implementations it makes more sense to call it so) types of Row/Column definitions, explained in detail here. You can see above that we iterate through the supplied values and append a new RowDefinition according to the value.

Note that if there is a parsing error, we are throwing a descriptive error message for clarity during the debug process.


That's it for the code! Now all that's left is to mark it up. Make sure that you reference your assembly in your directive:

<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"/>

Now you can define the RowDefinitions in XAML like this:

<Grid local:Application.GridRows="Auto,50,*,Auto" />

Which is the equivalent of:

<Grid>
        
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="50"/>
        <RowDefinition/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    
</Grid>

Or more importantly, you can define the RowDefinitions from within a Style, defined anywhere in your application as a resource:

<Style x:Key="StylizedRowDefinitions" TargetType="Grid">
    <Setter Property="local:Application.GridRows" Value="3*,5*,Auto"/>
</Style>
<Grid Style="{DynamicResource StylizedRowDefinitions}">
</Grid>

Now we will implement the GridColumnsProperty, and we can see all the code together:

C#:

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;
        }
    }
}

Now defining your Column Definitions is as simple as defining your RowDefinitions

<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>

Points of Interest

Stylizing Row/Column Definitions bridges a gap that exists in WPF scalability, and helps make your application more dynamic. Attached Properties are a vastly underrated way of achieving scalability with often very little code compared to other approaches.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章