Использование атрибутов для нормализации и валидации бизнес-сущностей
Страница 5. Создание класса атрибутов нормализации данных


 

Теперь мы создаём базовый класс, определяющий общие свойства и методы для всех атрибутов нормализации. Под нормализацией я понимаю приведение значения свойства к одному из возможных валидных(корректных) значений. Свойство NormalizationOrder будет объяснено чуть позже, а виртуальный метод Normalize это тот самый метод атрибута, который и будет выполнять основную работу. Он должен быть переопределен в дочерних классах.
/// <summary>

/// Базовый класс-атрибут для создания атрибутов нормализации

/// </summary>

[AttributeUsage(AttributeTargets.Property)]
public abstract class NormalizationBaseAttribute : Attribute
{
int m_normalizationOrder = 0;
/// <summary>

/// Порядковый номер при нормализации

/// </summary>

public virtual int NormalizationOrder
{
get {return m_normalizationOrder;}
}

public NormalizationBaseAttribute(int normalizationOrder)
{
m_normalizationOrder = normalizationOrder;
}

/// <summary>

/// Нормализовать указанное значение

/// </summary>

/// <param name="value">Значение для нормализации</param>

/// <returns>Нормализованный объект</returns>

public virtual object Normalize(object value)
{
return "#NormalizationBase - Normalize method not overrided!";
}
}
А теперь определим несколько атрибутов, которые мы будем практически использовать. Атрибут, выполняющий обрезание определенных символов с начала и/или конца строки. Обрезаемый символ по умолчанию - пробел.
/// <summary>

/// Атрибут нормализации - отсечение символов строки слева и/или справа

/// </summary>

public class NormalizationStringTrimAttribute : NormalizationBaseAttribute
{

#region members

char m_trimChar = ' ';
/// <summary>

/// Отсекаемый символ

/// </summary>

public virtual char TrimChar
{
get {return m_trimChar;}
set {m_trimChar = value;}
}

bool m_trimLeft = true;
/// <summary>

/// Отсекать слева (с начала строки)

/// </summary>

public virtual bool TrimLeft
{
get {return m_trimLeft;}
set {m_trimLeft = value;}
}

bool m_trimRight = true;
/// <summary>

/// Отсекать справа (с конца строки)

/// </summary>

public virtual bool TrimRight
{
get {return m_trimRight;}
set {m_trimRight = value;}
}

#endregion

public NormalizationStringTrimAttribute(int normalizationOrder) : base(normalizationOrder)
{}

public override object Normalize(object value)
{
if (m_trimChar == ' ')
{
if (m_trimLeft & m_trimRight)
return value.ToString().Trim();
else if (m_trimLeft)
return value.ToString().TrimStart();
else if (m_trimRight)
return value.ToString().TrimEnd();
else
return value;
}
else
{
if (m_trimLeft & m_trimRight)
return value.ToString().Trim(m_trimChar);
else if (m_trimLeft)
return value.ToString().TrimStart(m_trimChar);
else if (m_trimRight)
return value.ToString().TrimEnd(m_trimChar);
else
return value;
}
}
}
Ещё один атрибут - сведения нескольких пробелов к одному.
/// <summary>

/// Атрибут нормализации строки - сведение нескольких пробелов подряд к одному \
/// (максимально 24->1)

/// </summary>

public class NormalizationStringWhiteSpaceReduceAttribute : NormalizationBaseAttribute
{

public NormalizationStringWhiteSpaceReduceAttribute(int normalizationOrder):
base(normalizationOrder)
{}

public override object Normalize(object value)
{
return value.ToString().Replace(" ", " ").Replace(" ", " ").Replace(" ", " ");
}
}
Итак у нас есть 2 атрибута и с ними уже можно что-то сделать. Например написать такой код
[DisplayName("ФИО")]
[NormalizationStringTrim()]
[NormalizationStringWhiteSpaceReduce()]
public string Name
{
get { return m_Name;}
set { m_Name = value;}
}
то есть мы предполагаем что написанная нами функция получит все атрибуты нормализации для данного свойства и последовательно перебирая их обработает свойство. Однако возникает вопрос в какой последовательности они будут возвращены отражением (а это может быть очень важно)? В той ли в которой были объявлены в коде? К сожалению, ответ - 'Нет'. Приходится извращаться и добавлять параметр, который определял порядок(последовательность) применения атрибутов. Для этого и предназначено виртуальное свойство NormalizationOrder. То есть наш сценарий действий таков:
1 получаем для свойства массив атрибутов основанных на NormalizationBaseAttribute с помощью метода Attribute.GetCustomAttributes.
2 Сортируем его в требуемом порядке.
3 Последовательно применяем к объекту нормализацию.

Для второго пункта нам понадобится класс NormalizationComparer, предназначенный для сравнения двух атрибутов, наследующихся от NormalizationBaseAttribute. Сравнение производится по свойству NormalizationOrder.
/// <summary>

/// Класс-сравнитель для NormalizationBaseAttribute

/// (сравнивает по NormalizationOrder)

/// </summary>

public class NormalizationComparer : IComparer
{

#region IComparer Members

public int Compare(object x, object y)
{
return ((NormalizationBaseAttribute)x).NormalizationOrder.CompareTo(
((NormalizationBaseAttribute)y).NormalizationOrder );
}

#endregion

}
основная функция нормализации
/// <summary>

/// Нормализует поля объекта согласно установленным правилам

/// </summary>

public void Normalize()
{
// Получить атрибуты уровня свойств.

// Получить все свойства данного класса и поместить их в массив

PropertyInfo[] pInfo = this.GetType().GetProperties();

// атрибуты всех свойств класса

for (int j=0; j<pInfo.Length; j++)
{
Attribute[] atts = Attribute.GetCustomAttributes(pInfo[j], typeof(NormalizationBaseAttribute));
Array.Sort(atts, new NormalizationComparer());
for (int k=0; k<atts.Length; k++)
{
NormalizationBaseAttribute att = (NormalizationBaseAttribute)atts[k];
if (att != null)
pInfo[j].SetValue(this, att.Normalize(pInfo[j].GetValue(this, null)), null);
}
}
}
и небольшой тестовый код
/// <summary>

/// The main entry point for the application.

/// </summary>

[STAThread]
static void Main(string[] args)
{
Person p = new Person(-1);
p.ShowNames();
Console.ReadLine();

p.Name = " Иванов Иван Иванович ";
Console.WriteLine("'{0}'", p.Name);
<b>p.Normalize();</b>
Console.WriteLine("'{0}'", p.Name);
Console.ReadLine();
...

Как вы видите, оба атрибута отработали и привели наше свойство к требуемому виду.
Отлично! Но нормализацией следует пользоваться с осторожностью, ведь пользователь уверен, что ввел одно значение, а перед записью в БД произошла нормализация, и значение могло измениться... Поэтому возможно 3 варианта:
- проверять введенные пользователем данные и если они не соответствуют правильным предупреждать пользователя об этом и не позволять продолжать, пока они не будут исправлены;
- проверять введенные пользователем данные и если они не соответствуют правильным нормализовывать и извещать пользователя об этом;
- нормализовывать данные, а пользователю ничего не сообщать :)

Я предпочитаю второй вариант, если ошибок нет данные сохраняются и актуальные значения отображаются пользователю.

 
Следующая статья »