Фишки XAML-разработчика: динамический Grid +9


В статье рассмотрим несколько полезных усовершенствований для контрола Grid.
image

Классический сценарий использования Grid предполагает следующий синтаксис

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition MinHeight="20" Height="Auto"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="2*"/>
        <RowDefinition Height="*" MaxHeight="100"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition MinWidth="100" Width="*" MaxWidth="300"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    
    <TextBlock
    	Grid.Row="1"
    	Grid.Column="1"
    	Grid.RowSpan="1"
    	Grid.ColumnSpan="2">
    	
    <!--...-->
</Grid>

У него имеется ряд недостатков:
1. Падение лаконичности кода при усложнении интерфейса
2. Случается, при временной cмене типа контрола с Grid на StackPanel, например, необходимо удалять либо комментировать блоки декларации колонок и столбцов, что не всегда удобно
3. Такой Grid достаточно статичен и видоизменять его колонки со столбцами во время работы приложения не слишком сподручно и красиво в контексте паттерна MVVM

Однако существует весьма остроумный способ устранения этих недостатков. Взгляните на следующее расширение Rack (руск. «Стеллаж»)

<Grid Rack.Rows="* 20\Auto * 2* */100 * *" Rack.Columns="* 50\*/100 *">
    <TextBlock Rack.Set="R1 C1 RS1 CS2">
    <!--...-->
</Grid>

1. Код лаконичен
2. При замене типа контрола ничего не нужно комментировать или удалять
3. Доступна высокая степень динамичности интерфейса

<Grid 
    Rack.Rows="{Binding Property1, Converter={StaticResource RowsConverter}}" 
    Rack.Columns="{Binding Property1, Converter={StaticResource ColumnsConverter}}" >
    
    <TextBlock Rack.Set="{Binding Property1, Converter={StaticResource TextPositionConverter}}">
    <!--...-->
</Grid>

Поначалу такой синтаксис выглядит непривычно, но на деле он не сложнее, чем, скажем, объявление векторной геометрии для Path. В строке [Rack.Rows="* 20\Auto * 2* */100 * *"] через запятую либо пробел происходит декларация колонок [столбцов], а опциональные параметры «20\» и «/100» задают минимальные и максимальные размеры соответственно. В свою очередь [Rack.Set=«R1 C1 RS1 CS2»] означает присваивание значений свойствам Grid.Row, Grid.Column, Grid.RowSpan, Grid.ColumnSpan, причём все значения указывать не обязательно, то есть запись [Rack.Set=«R1 C1»] также верна.

Реализовано расширение через вложенные свойства (atteched properties) и включено в библиотеку Aero Framework [резервная ссылка]. Исходный код открыт, поэтому, если вам не нравится, к примеру, предложенный синтаксис, то вы запросто можете видоизменить его по своему усмотрению. Если вы скачаете библиотеку и запустите проект HelloAero, то воочию убедитесь, каким динамичным может стать обычный Grid с применением такого способа декларации. На всякий случай приведу пару скриншотов и исходный код ниже.

Спасибо за Ваше внимание!

Screenshots




Source Code
using System;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Controls;

namespace Aero.Markup
{
    public static class Rack
    {
        #region Declarations

        public static readonly DependencyProperty ShowLinesProperty = DependencyProperty.RegisterAttached(
            "ShowLines", typeof (bool), typeof (Rack), new PropertyMetadata(default(bool), (o, args) =>
            {
                var grid = o as Grid;
                if (grid == null) return;
                grid.ShowGridLines = Equals(args.NewValue, true);
            }));

        public static void SetShowLines(DependencyObject element, bool value)
        {
            element.SetValue(ShowLinesProperty, value);
        }

        public static bool GetShowLines(DependencyObject element)
        {
            return (bool) element.GetValue(ShowLinesProperty);
        }

        public static readonly DependencyProperty RowsProperty = DependencyProperty.RegisterAttached(
            "Rows", typeof (string), typeof (Rack), new PropertyMetadata("", OnRowsPropertyChanged));

        public static readonly DependencyProperty ColumnsProperty = DependencyProperty.RegisterAttached(
            "Columns", typeof (string), typeof (Rack), new PropertyMetadata("", OnColumnsPropertyChanged));

        public static string GetRows(DependencyObject d)
        {
            return (string) d.GetValue(RowsProperty);
        }

        public static void SetRows(DependencyObject d, string value)
        {
            d.SetValue(RowsProperty, value);
        }

        public static string GetColumns(DependencyObject d)
        {
            return (string) d.GetValue(ColumnsProperty);
        }

        public static void SetColumns(DependencyObject d, string value)
        {
            d.SetValue(ColumnsProperty, value);
        }

        public static readonly DependencyProperty SetProperty = DependencyProperty.RegisterAttached(
            "Set", typeof (string), typeof (Rack), new PropertyMetadata("", OnSetChangedCallback));

        public static void SetSet(DependencyObject element, string value)
        {
            element.SetValue(SetProperty, value);
        }

        public static string GetSet(DependencyObject element)
        {
            return (string) element.GetValue(SetProperty);
        }

        #endregion

        private static void OnRowsPropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            var grid = o as Grid;
            if (grid == null) return;

            grid.RowDefinitions.Clear();
            var patterns = (e.NewValue as string ?? "").Split(new[] {' ', ','}, StringSplitOptions.RemoveEmptyEntries);
            foreach (var pattern in patterns)
            {
                var indexMin = pattern.IndexOf(@"\", StringComparison.Ordinal);
                var indexMax = pattern.IndexOf(@"/", StringComparison.Ordinal);
                var hasMin = indexMin >= 0;
                var hasMax = indexMax >= 0;
                var valueMin = hasMin ? pattern.Substring(0, indexMin) : "";
                var valueMax = hasMax ? pattern.Substring(indexMax + 1, pattern.Length - indexMax - 1) : "";
                var start = hasMin ? indexMin + 1 : 0;
                var finish = hasMax ? indexMax : pattern.Length;
                var value = pattern.Substring(start, finish - start);
                var definition = new RowDefinition {Height = value.ToGridLength()};
                if (valueMin != "") definition.MinHeight = double.Parse(valueMin);
                if (valueMax != "") definition.MaxHeight = double.Parse(valueMax);
                grid.RowDefinitions.Add(definition);
            }
        }

        private static void OnColumnsPropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            var grid = o as Grid;
            if (grid == null) return;

            grid.ColumnDefinitions.Clear();
            var patterns = (e.NewValue as string ?? "").Split(new[] {' ', ','}, StringSplitOptions.RemoveEmptyEntries);
            foreach (var pattern in patterns)
            {
                var indexMin = pattern.IndexOf(@"\", StringComparison.Ordinal);
                var indexMax = pattern.IndexOf(@"/", StringComparison.Ordinal);
                var hasMin = indexMin >= 0;
                var hasMax = indexMax >= 0;
                var valueMin = hasMin ? pattern.Substring(0, indexMin) : "";
                var valueMax = hasMax ? pattern.Substring(indexMax + 1, pattern.Length - indexMax - 1) : "";
                var start = hasMin ? indexMin + 1 : 0;
                var finish = hasMax ? indexMax : pattern.Length;
                var value = pattern.Substring(start, finish - start);
                var definition = new ColumnDefinition {Width = value.ToGridLength()};
                if (valueMin != "") definition.MinWidth = double.Parse(valueMin);
                if (valueMax != "") definition.MaxWidth = double.Parse(valueMax);
                grid.ColumnDefinitions.Add(definition);
            }
        }

        private static void OnSetChangedCallback(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            var element = o as FrameworkElement;
            if (element == null) return;
            var patterns = (e.NewValue as string ?? "").Split(new[] {' ', ','}, StringSplitOptions.RemoveEmptyEntries);
            var columnPattern = patterns.FirstOrDefault(p => p.StartsWith("C") && !p.StartsWith("CS")).With(p => p.Replace("C", ""));
            var rowPattern = patterns.FirstOrDefault(p => p.StartsWith("R") && !p.StartsWith("RS")).With(p => p.Replace("R", ""));
            var columnSpanPattern = patterns.FirstOrDefault(p => p.StartsWith("CS")).With(p => p.Replace("CS", ""));
            var rowSpanPattern = patterns.FirstOrDefault(p => p.StartsWith("RS")).With(p => p.Replace("RS", ""));
            int column, row, columnSpan, rowSpan;
            if (int.TryParse(columnSpanPattern, out columnSpan)) Grid.SetColumnSpan(element, columnSpan);
            if (int.TryParse(rowSpanPattern, out rowSpan)) Grid.SetRowSpan(element, rowSpan);
            if (int.TryParse(columnPattern, out column)) Grid.SetColumn(element, column);
            if (int.TryParse(rowPattern, out row)) Grid.SetRow(element, row);
        }

        private static GridLength ToGridLength(this string length)
        {
            try
            {
                length = length.Trim();
                if (length.ToLowerInvariant().Equals("auto")) return new GridLength(0, GridUnitType.Auto);
                if (!length.Contains("*")) return new GridLength(double.Parse(length), GridUnitType.Pixel);
                length = length.Replace("*", "");
                if (string.IsNullOrEmpty(length)) length = "1";
                return new GridLength(double.Parse(length), GridUnitType.Star);
            }
            catch (Exception exception)
            {
                Debug.WriteLine(exception.Message);
                return new GridLength();
            }
        }
    }
}


Update
Применение
Динамический Grid может оказаться очень полезным при разработке сложных интерфейсов, а также, например, в случаях, когда приложение поддерживает портретную и альбомную ориентацию, в зависимости от которой визуальные элементы нужно компоновать слегка по-разному, а создавать новую страницу во-многом с дублирующимся кодом не имеет смысла.




К сожалению, не доступен сервер mySQL