Применение атрибутов в C#

    Большинство источников по использованию атрибутов [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 — свойство к тексту
    Share post

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 28

      –1
      Картинка немного не в тему, а так — статья полезная. Хотя было бы неплохо ознакомить людей с кастомными атрибутами, бывает полезно.
        0
        Дело в том, что большинство статей про атрибуты рассказывают про то, как можно создать пользовательские (кастомные) атрибуты. Но вот как их потом применить… Именно поэтому я и использовал только стандартные, которые идут в самом Framework. В принципе можно написать пример и на использование кстомных атрибутов, например, сделать атрибут по средством которого можно было бы проверять, что два поля заполнены одинаковыми значениями…
          0
          Это косательно форм. А допустим, контроль доступа, маршрутизация для запросов в ASP.NET через атрибуты — это было бы интереснее. К слову, я даже не знаю можно ли последнее через атрибуты задать.
            0
            делал такое через атрибуты на PostSharp.
        0
        Это сила привычки или пример на ASP.NET MVC выглядел бы попроще?
          0
          В ASP .Net MVC есть стандартная инфраструктура, для работы с атрибутами такого вида. В данной статье, я пытался привести пример того, как написать такую инфраструктуру самому.
          0
          могут быть получены при помощиотражений
          Ну вот кем надо было быть, чтобы перевести в этом контексте Reflection как отражение? Кого вообще нанимали для перевода MSDN? Они хоть иногда читали книжки кроме букваря? Должно быть, нет, ибо иначе они бы знали, что есть такое слово, как рефлексия, которое хоть и является заимствованием с латыни, но уже давно и прочно укоренилось в русском языке, а смысл передаёт наиболее точно. Хочется взять и расстрелять просто.
            0
            Именно по этому оба раза эта фраза взята в кавычки.
              +1
              А я как-то привык к «отражению», «рефлексия» — звучит как психологический термин )
                +6
                Суть в том, что в оригинале этот психологический термин и был выбран в качестве названия неймспейса (самокопание же). Но MSDN, видимо, переводили те же люди, что обозвали overmind надмозгом.
                0
                Все же дело вкуса. Не стоит так резко.
                +1
                Каждый раз дергать рефлексию — дело накладное. В подобном приложении это, конечно, несерьезно, а вот когда атрибуты используются где-то, где нужна скорость, имеет смысл предобработать их и сгенерить на лету классы-обработчики, имхо стоит этот момент отметить. Где-то я видел статью хорошую, со сравнением различных методов обработки атрибутов и скорости этих методов.
                Я, допустим, использовал атрибуты в самописном движке для одной игры — ими помечались поля, подлежащие сериализации и передаче по сети. Атрибуты предобрабатывались при старте приложения и на их основе генерился индивидуальный сериалайзер.
                Это давало существенный выигрыш в рантайме (ценой слегка увеличенного времени загрузки).
                  0
                  Хм, а это ведь идея для отдельной статьи. Напишите?
                  Кстати, будет забавно показать практику из генетического программирования. Когда при старте приложения, на основе атрибутов, создаются cs файлы с новыми классами, они компилируются и подгружаются в приложение.
                    0
                    В том-то и дело, что эту статью я уже где-то видел, вроде бы, на хабре.
                    Там было очень подробно и хорошо расписано, когда и как имеет смысл обращаться к рефлексии, вот ссылку на нее бы сюда добавить.
                    Но увы, не могу вспомнить как она называлась.
                    0
                    Генерить классы — дело накладное. Особенно когда на других версиях рантайма софтина начинает валиться ругаясь на невалидный IL.
                      0
                      Так, PEVerify'ить надо сначала. И тестировать везде, где генератор может работать.
                      И с чего, вдруг, накладное-то, кстати? Накладное — через CodeDom, да и то несущественно, если все делается при запуске приложения или по первому обращению. Reflection.Emit же вообще охренительно быстр.
                        0
                        Накладное в плане времени написания и тестирования, понятное дело, что в рантайме потом летает. Просто у меня был печальный опыт, когда писалась достаточно сложная штука через этот самый emit, а потом оказалось, что JIT от Microsoft несколько отличается от используемого в моно. Очень долго искал, что именно ему не нравится, т. к. исключение было очень малоинформативным.
                          0
                          А, с этим согласен. Поддержка кода, который генерирует IL — это как владение черной магией :) Поэтому, лучше без крайностей, юзать только там, где это необходимо, а не для всего подряд.
                      0
                      Зависит от того, что подразумевается под «дергать рефлексию». Reflection — такой же инструмент, как и остальные, имеющий свои достоинства и недостатки, и предназначен для использования в одних случаях и не предназначен для других. Расхожее мнение «Reflection — это нечто очень медленное» (в общем случае) имеет мало общего с практикой.
                        0
                        А я про что сказал?
                        Такой же инструмент. Имеет достоинства и недостатки. Один из недостатков — весьма вероятное снижение скорости выполнения приложения в отдельных случаях, к которым приведенный автором пример не относится. Но это не единственный пример применения, и во многих случаях следует не использовать рефлексию «в лоб», а воспользоваться слегка другим подходом.
                      0
                      Спасибо, взял на заметку
                        0
                        Если писать про валидацию в wpf, то как можно обойтись без IDataErrorInfo и NotifyOnValidationError=True в {Binding}?
                          +2
                          Коли ночь на дворе, а сна нет, расскажу подробнее, как это делаю я. Материал много где дублируется в сети, и, скорее всего, не будет откровением, но кому-то может пригодиться.

                          Во ViewModel, которую мы хотим валидировать, реализуем интерфейс IDataErrorInfo. В этом интерфейсе два метода: один позволяет указать на ошибку в конкретной проперти, другой отвечает за валидацию модели в целом. Начну с валидации одного свойства.
                          public virtual string this[string columnName]
                          {
                          get { return AttributesValidation.Validate(this, columnName); }
                          }

                          На вход этого… забыл слово… подаётся название свойства, которое надо валидировтаь. На выходе — либо строка с описанием ошибки, либо string.Empty.
                          Для валидации аттрибутами у меня есть небольшой класс-хелпер:
                          internal static class AttributesValidation
                          {
                          public static string Validate(IDataErrorInfo source, string columnName)
                          {
                          var type = source.GetType();
                          var property = type.GetProperty(columnName);
                          var validators = (ValidationAttribute[]) property.GetCustomAttributes(typeof (ValidationAttribute), true);
                          if (validators.Any())
                          {
                          var value = property.GetValue(source, null);
                          var errors = validators.Where(v => !v.IsValid(value)).Select(v => v.ErrorMessage ?? "").ToArray();
                          return string.Join(Environment.NewLine, errors);
                          }
                          return string.Empty;
                          }
                          }

                          Все валидаторы, которые есть, они наследуются от System.ComponentModel.DataAnnotations.ValidationAttribute.

                          Ну а теперь это всё необходимо обработать в WPF:
                          [TextBox Text="{Binding Path=Name, Mode=TwoWay, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" /]

                          Для несложных проверок мне нравится ставить UpdateSourceTrigger=PropertyChanged. В этом случае (длят екстбокса) валидация происходит после каждого нажатия клавиши, а не после потери фокуса. Это нагляднее, но больше обработок и вычислений.
                          Этот снипет даёт красную рамку вокруг невалидных полей и вызов обработчика ошибок в валидаторе формы. То есть, можно сделать так:
                          class MyForm{
                          int _errCount;

                          public MyForm()
                          {
                          Validation.AddErrorHandler(this, OnChildControlError); // и на каждое появление ошибки валидации или пропадание ошибки валидации будет вызываться OnChildControlError.
                          }

                          protected void OnChildControlError(object sender, ValidationErrorEventArgs e)
                          {
                          switch (e.Action)
                          {
                          case ValidationErrorEventAction.Added:
                          _errCount += 1;
                          break;
                          case ValidationErrorEventAction.Removed:
                          _errCount -= 1;
                          break;
                          default:
                          throw new ArgumentOutOfRangeException();
                          }
                          // теперь в переменной _errCount у нас записано количество ошибочных полей, и мы можем делать какое-то действие, например задизаблить кнопку OK. Я предпочитаю объявить DependencyProperty Valid, и с ним работать в xaml.
                          Valid = _errCount == 0;
                          }

                          Таким образом, если всё это сплести в одну кучу, то мы получаем:
                          1. ViewModel умеет сообщать о своих ошибках через [propertyName]
                          2. Специальный валидатор умеет валидировать модели через атрибуты
                          3. xaml умеет помечать красным невалидные филды
                          4. Viewшка (например, форма или UserControl) умеет считать невалидные филды внутри себя и имеет DependcyProperty Valid, к которому можно биндить Enabled-свойство кнопок.

                          Вывод сообщений об ошибке в xaml я не буду расписывать, так как это есть в msdn и stackoverflow, а комментарий итак получается очень большим.
                            0
                            ух-ты, парсер-молодец. Вечно забываю, что в теге code дублируются переносы строк. Прошу прощения за портянку.
                              0
                              Это слово «индексатор»?
                              На хабре, для оформления кода можно использовать вмето тега code — тег source, с атрибутом lang=«c#» или lang=«cs» (c# иногда глючит).
                                0
                                Про тег source помнить надо, а code вот тут, прямо в этой коробке на кнопке вверху сделан.
                                  0
                                  Это да…
                                0
                                Простите за дубль, но глаза не выдерживали читать код без соответстсвующей маркировки, использовал source. Тому кто выйдет на эту статью как я, будет немного проще. Дальше оригинал:

                                Коли ночь на дворе, а сна нет, расскажу подробнее, как это делаю я. Материал много где дублируется в сети, и, скорее всего, не будет откровением, но кому-то может пригодиться.

                                Во ViewModel, которую мы хотим валидировать, реализуем интерфейс IDataErrorInfo. В этом интерфейсе два метода: один позволяет указать на ошибку в конкретной проперти, другой отвечает за валидацию модели в целом. Начну с валидации одного свойства.
                                public virtual string this[string columnName]
                                {
                                   get { return AttributesValidation.Validate(this, columnName); }
                                }
                                

                                На вход этого… забыл слово… подаётся название свойства, которое надо валидировтаь. На выходе — либо строка с описанием ошибки, либо string.Empty.
                                Для валидации аттрибутами у меня есть небольшой класс-хелпер:
                                internal static class AttributesValidation
                                {
                                   public static string Validate(IDataErrorInfo source, string columnName)
                                   {
                                   var type = source.GetType();
                                   var property = type.GetProperty(columnName);
                                   var validators = (ValidationAttribute[]) property.GetCustomAttributes(typeof (ValidationAttribute), true);
                                   if (validators.Any())
                                      {
                                         var value = property.GetValue(source, null);
                                         var errors = validators.Where(v => !v.IsValid(value)).Select(v => v.ErrorMessage ?? "").ToArray();
                                         return string.Join(Environment.NewLine, errors);
                                      }
                                   return string.Empty;
                                   }
                                }
                                

                                Все валидаторы, которые есть, они наследуются от System.ComponentModel.DataAnnotations.ValidationAttribute.

                                Ну а теперь это всё необходимо обработать в WPF:
                                [TextBox Text="{Binding Path=Name, Mode=TwoWay, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" /]
                                

                                Для несложных проверок мне нравится ставить UpdateSourceTrigger=PropertyChanged. В этом случае (длят екстбокса) валидация происходит после каждого нажатия клавиши, а не после потери фокуса. Это нагляднее, но больше обработок и вычислений.
                                Этот снипет даёт красную рамку вокруг невалидных полей и вызов обработчика ошибок в валидаторе формы. То есть, можно сделать так:
                                class MyForm{
                                int _errCount;
                                
                                public MyForm()
                                {
                                   Validation.AddErrorHandler(this, OnChildControlError); // и на каждое появление ошибки валидации или пропадание ошибки валидации будет вызываться OnChildControlError.
                                }
                                
                                protected void OnChildControlError(object sender, ValidationErrorEventArgs e)
                                {
                                   switch (e.Action)
                                   {
                                   case ValidationErrorEventAction.Added:
                                      _errCount += 1;
                                      break;
                                   case ValidationErrorEventAction.Removed:
                                      _errCount -= 1;
                                      break;
                                   default:
                                      throw new ArgumentOutOfRangeException();
                                   }
                                   // теперь в переменной _errCount у нас записано количество ошибочных полей, и мы можем делать какое-то действие, например задизаблить кнопку OK. Я предпочитаю объявить DependencyProperty Valid, и с ним работать в xaml. 
                                   Valid = _errCount == 0;
                                }
                                

                                Таким образом, если всё это сплести в одну кучу, то мы получаем:
                                1. ViewModel умеет сообщать о своих ошибках через [propertyName]
                                2. Специальный валидатор умеет валидировать модели через атрибуты
                                3. xaml умеет помечать красным невалидные филды
                                4. Viewшка (например, форма или UserControl) умеет считать невалидные филды внутри себя и имеет DependcyProperty Valid, к которому можно биндить Enabled-свойство кнопок.

                                Вывод сообщений об ошибке в xaml я не буду расписывать, так как это есть в msdn и stackoverflow, а комментарий итак получается очень большим.

                            Only users with full accounts can post comments. Log in, please.