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

  • Tutorial
В статье рассмотрим несколько полезных усовершенствований для контрола 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 может оказаться очень полезным при разработке сложных интерфейсов, а также, например, в случаях, когда приложение поддерживает портретную и альбомную ориентацию, в зависимости от которой визуальные элементы нужно компоновать слегка по-разному, а создавать новую страницу во-многом с дублирующимся кодом не имеет смысла.
Метки:
Поделиться публикацией
Похожие публикации
Комментарии 11
  • 0
    Спасибо, интересный велосипед, конечно, но ничего принципиально нового. Всё так же мы объявляем номер столбца/строки для каждого элемента.
    Лично мне было бы интересно вот что
    Допустим, есть у нас грид с 10-ю строками. В каждой по много элементов. И вот нам понадобилось удрать строку №2 со всем её содержимым.
    Как решить:
    1 — удалить элементы из строки №2 и сделать её рамер = 0. Плохо, засоряем код.
    2 — удалить элементы из строки №2 и саму строку тоже, а для элементов из нижних строк уменьшить Grid.Row на 1. Очень неудобно, т.к. элементов много.
    Хотелось бы иметь возможность указывать номер строки/столбца не абсолютно, а как-нибудь относительно.
    У самого пока руки не доходят, может Вам будет интересно :)
    • 0
      Blend — лучше для WPF-ера нет. Он сделает это автоматом.
      Правда адекватным бленд стал только в новой версии.
      • 0
        Спасибо. Попробую.
      • 0
        Думал над вашим вопросом, и пришёл к выводу, что, возможно, не совсем вас понял.
        Вы имеете в виду удаление элементов из строки во время работы XAML-дизайнера среды разработки?

        Просто в статье описан механизм динамической смены состояния Grid именно во время работы приложения…
        Это может быть полезным для сложных интерфейсов, например, когда приложение поддерживает портретную и альбомную ориентацию, и в зависимости от неё визуальные элементы нужно компоновать слегка по-разному, а создавать новую страницу с дублирующимся кодом не имеет смысла.
        • 0
          Да, я имею в виду редактирование разметки, не рантайм. Чтобы удалить строку, приходится редактировать Grid.Row элементов из строк ниже. Но тут уже предложили решения:
          1. Говорят, Blend умный и умеет сам менять номера строк, когда надо.
          2. StackGrid из Catel.
      • 0
        Правильнее, на мой взгляд, установить у ненужного элемента свойство Visibility в состояние Collased, либо же использовать ItemsControl. Даже если вы удалите строку номер два, то сам контрол останется в визуальном дереве и код останется засорён, если это можно так назвать.

        Хотелось бы иметь возможность указывать номер строки/столбца не абсолютно, а как-нибудь относительно.

        В статье описан сам принцип и базовый синтаксис, усовершенствовать реализацию можно как угодно, на что хватит фантазии :)
        • 0
          Код не стал лаконичнее. Даже наоборот, для новых сотрудников, читать подобное и каждый раз вспоминать становится сложнее.
          + ReSharper умеет работать с XAML и рефакторить в том числе колнки и строки у грида и его элементов. В вашем случае мы теряем такую возможность.
          • 0
            Решарпер, да, не понимает такую запись, но это не столь критичный недостаток.
            Код не стал лаконичнее.

            Конечно, у вас своё видение, но, по-моему, он стал компактнее в разы да и сложность его не такая высокая :)
          • +1
            На мой взгляд, лаконичнее выглядит нечто вроде StackGrid из Catel. В нём строки и столбцы задаются так же, как и в обычном Grid, а вот у содержимого задавать свойства уже необязательно, оно само раскладывается по ячейкам в порядке объявления. Есть также вспомогательные элементы EmptyCell, EmptyRow и EmptyColumn. Логики там на 2 экрана, можно и самому реализовать, если весь Catel тянуть не хочется.
            • 0
              Динамичным сделать такой грид ещё сложнее, чем обычный…
              • 0
                О, а вот это похоже на то, о чём я писал в первом комментарии. Спасибо.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.