Большинство источников по использованию атрибутов [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. Любопытный читатель, может поинтересоваться: «А причем тут картинка в шапке?». Ну, я хотел бы думать, что это намек, на то, что атрибуты, как и этот знак, вроде бы есть, но вроде бы и нет.
Источники:
1. MSDN — Атрибуты (C# и Visual Basic) к тексту
2. dotsite — Атрибуты и их использование в C# к тексту
3. MSDN — Отражение (C# и Visual Basic) к тексту
4. MSDN — NonSerializedAttribute — класс к тексту
5. MSDN — Binding.UpdateSourceTrigger — свойство к тексту
