Pull to refresh

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

Reading time 6 min
Views 96K
Большинство источников по использованию атрибутов [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 — свойство к тексту
Tags:
Hubs:
+15
Comments 28
Comments Comments 28

Articles