В статье рассмотрим несколько полезных усовершенствований для контрола Grid.
Классический сценарий использования Grid предполагает следующий синтаксис
У него имеется ряд недостатков:
1. Падение лаконичности кода при усложнении интерфейса
2. Случается, при временной cмене типа контрола с Grid на StackPanel, например, необходимо удалять либо комментировать блоки декларации колонок и столбцов, что не всегда удобно
3. Такой Grid достаточно статичен и видоизменять его колонки со столбцами во время работы приложения не слишком сподручно и красиво в контексте паттерна MVVM
Однако существует весьма остроумный способ устранения этих недостатков. Взгляните на следующее расширение Rack (руск. «Стеллаж»)
1. Код лаконичен
2. При замене типа контрола ничего не нужно комментировать или удалять
3. Доступна высокая степень динамичности интерфейса
Поначалу такой синтаксис выглядит непривычно, но на деле он не сложнее, чем, скажем, объявление векторной геометрии для 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 с применением такого способа декларации. На всякий случай приведу пару скриншотов и исходный код ниже.
Спасибо за Ваше внимание!
Update
Применение
Динамический Grid может оказаться очень полезным при разработке сложных интерфейсов, а также, например, в случаях, когда приложение поддерживает портретную и альбомную ориентацию, в зависимости от которой визуальные элементы нужно компоновать слегка по-разному, а создавать новую страницу во-многом с дублирующимся кодом не имеет смысла.
Классический сценарий использования 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 может оказаться очень полезным при разработке сложных интерфейсов, а также, например, в случаях, когда приложение поддерживает портретную и альбомную ориентацию, в зависимости от которой визуальные элементы нужно компоновать слегка по-разному, а создавать новую страницу во-многом с дублирующимся кодом не имеет смысла.
IL_Agent
Спасибо, интересный велосипед, конечно, но ничего принципиально нового. Всё так же мы объявляем номер столбца/строки для каждого элемента.
Как решить:
1 — удалить элементы из строки №2 и сделать её рамер = 0. Плохо, засоряем код.
2 — удалить элементы из строки №2 и саму строку тоже, а для элементов из нижних строк уменьшить Grid.Row на 1. Очень неудобно, т.к. элементов много.
Хотелось бы иметь возможность указывать номер строки/столбца не абсолютно, а как-нибудь относительно.
У самого пока руки не доходят, может Вам будет интересно :)
EngineerSpock
Blend — лучше для WPF-ера нет. Он сделает это автоматом.
Правда адекватным бленд стал только в новой версии.
IL_Agent
Спасибо. Попробую.
Makeman
Думал над вашим вопросом, и пришёл к выводу, что, возможно, не совсем вас понял.
Вы имеете в виду удаление элементов из строки во время работы XAML-дизайнера среды разработки?
Просто в статье описан механизм динамической смены состояния Grid именно во время работы приложения…
Это может быть полезным для сложных интерфейсов, например, когда приложение поддерживает портретную и альбомную ориентацию, и в зависимости от неё визуальные элементы нужно компоновать слегка по-разному, а создавать новую страницу с дублирующимся кодом не имеет смысла.
IL_Agent
Да, я имею в виду редактирование разметки, не рантайм. Чтобы удалить строку, приходится редактировать Grid.Row элементов из строк ниже. Но тут уже предложили решения:
1. Говорят, Blend умный и умеет сам менять номера строк, когда надо.
2. StackGrid из Catel.
Makeman
Правильнее, на мой взгляд, установить у ненужного элемента свойство Visibility в состояние Collased, либо же использовать ItemsControl. Даже если вы удалите строку номер два, то сам контрол останется в визуальном дереве и код останется засорён, если это можно так назвать.
В статье описан сам принцип и базовый синтаксис, усовершенствовать реализацию можно как угодно, на что хватит фантазии :)
Shersh
Код не стал лаконичнее. Даже наоборот, для новых сотрудников, читать подобное и каждый раз вспоминать становится сложнее.
+ ReSharper умеет работать с XAML и рефакторить в том числе колнки и строки у грида и его элементов. В вашем случае мы теряем такую возможность.
Makeman
Решарпер, да, не понимает такую запись, но это не столь критичный недостаток.
Конечно, у вас своё видение, но, по-моему, он стал компактнее в разы да и сложность его не такая высокая :)
darkdaskin
На мой взгляд, лаконичнее выглядит нечто вроде StackGrid из Catel. В нём строки и столбцы задаются так же, как и в обычном Grid, а вот у содержимого задавать свойства уже необязательно, оно само раскладывается по ячейкам в порядке объявления. Есть также вспомогательные элементы EmptyCell, EmptyRow и EmptyColumn. Логики там на 2 экрана, можно и самому реализовать, если весь Catel тянуть не хочется.
Makeman
Динамичным сделать такой грид ещё сложнее, чем обычный…
IL_Agent
О, а вот это похоже на то, о чём я писал в первом комментарии. Спасибо.