Большинство источников по использованию атрибутов [1, 2] рассказывают, что они есть, «обеспечивают эффективный способ связывания метаданных или декларативной информации с кодом», могут быть получены при помощи отражений [3]. В рамках данной статьи, я попробую показать прикладной пример применения атрибутов: проверка заполненности обязательных полей на форме добавления/редактирования нового бизнес-объекта.
Перед тем, как вы нажмете подробнее, несколько предупреждений:
1. Если вы уже работали с атрибутами, то, возможно, вам будет неинтересно.
2. При написании демонстрационного примера были допущены существенные упрощения (например, отказ от MVVM), с целью облегчения восприятия материала про атрибуты.
Итак начнем. Как я уже привел чуть выше: «Атрибуты обеспечивают эффективный способ связывания метаданных или декларативной информации с кодом». Что же такое эти самые метаданные? В большинстве случаев, это просто дополнительная информация о классе, свойстве или методе, которая на работу класса, свойства или метода не влияет. Но вот внешние, по отношению к нему, объекты приложения эту информацию могут получать и как то обрабатывать. Одним из ярких примеров применения атрибутов может служить атрибут NonSerializedAttribute [4]. Данным атрибутом вы можете пометить поле своего класса и оно будет работать абсолютно так же, как и до пометки. Но если вы решите воспользоваться сериализатором уже имеющимся в инфраструктуре .Net, то данное поле в выходную последовательность не попадет.
Ладно, про атрибуты чуть рассказал, по ссылкам кто хотел почитал, давайте собственно перейдем к примеру.
В качестве примера рассмотрим простую задачу ведения списка людей. Для хранения информации о человеке воспользуемся классом вот такого вида:
public class Person
{
[DisplayAttribute(Name="Фамилия")]
[RequiredAttribute()]
public string LastName { get; set; }
[Display(Name = "Имя")]
[Required()]
public string FirstName { get; set; }
[Display(Name = "Отчество")]
public string Patronym { get; set; }
}
В данном примере прошу обратить внимание, на два момента:
1. При использовании атрибутов «суффикс»: Attribute, можно не писать.
2. Фамилия и имя помечены атрибутом RequiredAttribute, а отчество нет.
Если мы с вами попробуем создать объект класса Person, то, как бы это обидно не звучало, мы его сможем создать с пустыми полями LastName и FirstName, т.к. этот атрибут ну совсем никак не влияет на поведение класса. Зачем тогда он? А мы им воспользуемся в форме добавления/редактирования человека, чтобы пользователь не мог закончить редактирование, пока эти поля не заполнены.
Общий вид приложения будет вот такой:
Для редактирования свойств человека воспользуемся UserControl (почему не формой чуть ниже):
<UserControl x:Class="AttributeExample.PersonEditor"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel>
<TextBlock Text="Фамилия" />
<TextBox Text="{Binding LastName,UpdateSourceTrigger=Explicit}" />
<TextBlock Text="Имя" />
<TextBox Text="{Binding FirstName,UpdateSourceTrigger=Explicit}" />
<TextBlock Text="Отчество" />
<TextBox Text="{Binding Patronym,UpdateSourceTrigger=Explicit}" />
</StackPanel>
</UserControl>
В cs файл этого UserControl-а даже не лезем. Обратили внимание, на то, что Binding к источнику применяется по внешнему событию? [5]
На текущий момент, у нас уже есть класс описывающий бизнес-объект и компонент, для редактирования свойств этого объекта. Осталось сделать универсальное окно, которое сможет показывать компоненты для редактирования бизнес-объектов и будет проверять заполненность обязательных полей, а также, если все заполнено правильно, будет применять Binding визуальных компонентов к полям бизнес-объектов.
Создаем форму:
<Window x:Class="AttributeExample.IngeniousWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="IngeniousWindow" MinWidth="300" SizeToContent="WidthAndHeight" >
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Grid.Row" Value="2" />
<Setter Property="HorizontalAlignment" Value="Right" />
<Setter Property="Width" Value="100" />
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="1*" />
<RowDefinition Height="35" />
</Grid.RowDefinitions>
<!--Компонент для показа ошибок-->
<StackPanel x:Name="spErrors" Visibility="Collapsed" Background="#FFCCCC">
<TextBlock Text="Незаполнены поля:" />
<ListView x:Name="lvProperties" Background="#FFCCCC" />
</StackPanel>
<!--Место для показа компонента-->
<ContentPresenter Grid.Row="1" x:Name="cpEditor" />
<!--Кнопки принять и отмена-->
<Button Margin="5" x:Name="btCancel" Content="Отмена" Click="btCancel_Click" />
<Button Margin="5,5,110,5" x:Name="btApply" Content="Принять" Click="btApply_Click" />
</Grid>
</Window>
Правим конструктор:
public partial class IngeniousWindow : Window
{
FrameworkElement _controlForShow = null;
public IngeniousWindow(FrameworkElement p_controlForShow)
{
InitializeComponent();
_controlForShow = p_controlForShow;
cpEditor.Content = p_controlForShow;
}
}
Добавляем обработчик на кнопку отмена:
private void btCancel_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
}
И самый интересный в данном примере обработчик кнопки принять:
private void btApply_Click(object sender, RoutedEventArgs e)
{
List<string> requiredPropertyNames = new List<string>();
// Получаем все TextBox c показываемого компонента
List<TextBox> textBoxes = GetChildTextBoxes(_controlForShow);
List<BindingExpression> expressions = new List<BindingExpression>();
// Получаем информацию о типе бизнес-объекта который редактируем
Type buisnesObjectType = _controlForShow.DataContext.GetType();
// Пробегаем и проверяем, являются ли они обязательными к заполнению
foreach (var item in textBoxes)
{
// Получаем Binding
BindingExpression expression = item.GetBindingExpression(TextBox.TextProperty);
if (expression != null)
{
expressions.Add(expression);
// Получаем свойство
PropertyInfo property = buisnesObjectType.GetProperty(expression.ParentBinding.Path.Path);
// Проверяем есть ли у него атрибут обязательности
Attribute attr = property.GetCustomAttribute(typeof(RequiredAttribute));
if (attr != null && string.IsNullOrWhiteSpace(item.Text))
{
// Атрибут есть, а в TextBox пустая строка, пытаемся получить описание
string propertyName = property.Name;
Attribute description = property.GetCustomAttribute(typeof(DisplayAttribute));
if (description != null)
{
propertyName = (description as DisplayAttribute).Name;
}
requiredPropertyNames.Add(propertyName);
}
}
}
// Если ошибок нет, то применяем Binding и закрываем окно
if (requiredPropertyNames.Count == 0)
{
foreach (var exp in expressions)
{
exp.UpdateSource();
}
DialogResult = true;
}
else
{
// Иначе, показываем список незаполненных полей
lvProperties.ItemsSource = requiredPropertyNames;
spErrors.Visibility = Visibility.Visible;
}
}
Вроде в комментариях все подробно описал, единственно метод: GetChildTextBoxes, приводить не буду, он пробегает по визуальному дереву и выбирает все TextBox-ы. Кому интересно, может его посмотреть, скачав исходники.
Все. Прикручиваем на главной форме обработчики к кнопкам добавить и редактировать:
private void btAdd_Click(object sender, RoutedEventArgs e)
{
Person person = new Person();
PersonEditor editor = new PersonEditor() { DataContext = person };
IngeniousWindow window = new IngeniousWindow(editor);
if (window.ShowDialog().Value)
{
_people.Add(person);
}
}
private void btEdit_Click(object sender, RoutedEventArgs e)
{
if (lvPeople.SelectedItem != null)
{
Person person = lvPeople.SelectedItem as Person;
PersonEditor editor = new PersonEditor() { DataContext = person };
IngeniousWindow window = new IngeniousWindow(editor);
window.ShowDialog();
}
}
Ну и вот так это выглядит:
Исходники, если кто не увидел ссылки в тексте, можно скачать тут (Проект в VS 11, если что).
P.s. Любопытный читатель, может поинтересоваться: «А причем тут картинка в шапке?». Ну, я хотел бы думать, что это намек, на то, что атрибуты, как и этот знак, вроде бы есть, но вроде бы и нет.
P.p.s. К сожалению, цветовое оформление стандартным тегом source, не может раскрасить код, после вставки примеров на XAML.
Источники:
1. MSDN — Атрибуты (C# и Visual Basic) к тексту
2. dotsite — Атрибуты и их использование в C# к тексту
3. MSDN — Отражение (C# и Visual Basic) к тексту
4. MSDN — NonSerializedAttribute — класс к тексту
5. MSDN — Binding.UpdateSourceTrigger — свойство к тексту
Автор: Teacher