Часто тут и там мы слышим о концепции отделения бизнес-логики приложения от UI. Разные софтверные компании предлагают разные решения этой задачи. Microsoft, к примеру, продвигает технологию WPF, а для Qt разрабатывается QML. В этой статье я хочу познакомит Вас с решением от компании Developer ExpresseXpress Application Framework (или просто XAF). Фреймворк предоставляет невероятно много функционала, поэтому я расскажу только о вершине айсберга. Возможно, в следующих статьях, я расскажу больше.
Сразу же стоит отметить, что XAF — это фреймворк для .NET. Поэтому, если Вы хотите написать кроссплатформенное приложение, то этот фреймворк не для Вас. Под Mono XAF тоже не работает.
Помимо минусов, у XAF есть и положительные качества. К примеру, XAF реализован для WinForms и ASP.NET приложений таким образом, что Вам не нужно задумываться, для какой платформы Вы пишите, конечный продукт будет работать на обеих платформах.
Для того, чтобы начать программировать с использованием XAF не нужно много знать о его устройстве, достатьчно прочесть tutorial на официальном сайте продукта. После установки фреймворка на компьютер, в Visual Studio появляются мастера создания проектов. Вы можете создать проект для WinForms, для ASP.NET или для обеих платформ сразу. Приложения XAF имеют модульную архитектуру.  Мастер сгенерирует несколько проектов, один из которых будет являться общим для всех приложений модулем. Также будут созданы модули для win и web приложений отдельно.
Все эти шаги подробно описаны в указанном tutorial. В этой же статье я приведу пример создания XAF приложения полностью вручную. Скачать демо версию фреймворка Вы можете с официально сайта.

База данных

Для работы приложения XAF Вам понадобится база данных на одной из поддерживаемых СУБД. В приведённом примере я буду использовать MySQL. Придумайте название для Вашего приложения и создайте базу данных с этим именем. Настоятельно рекомендую использовать кодировку UTF-8 для вновь созданной базы MySQL. Мой пример будет носить имя «XafDemo».

Создание базового приложения

Теперь, когда мы обзавелись базой данных, можно приступать к созданию приложения. Для этого создадим в студии каркас Windows Form Application и выкинем из него всё лишнее, оставив только файл Program.cs и ссылку на модуль System. Теперь добавим нужные нам ссылки:

  • System.configuration
  • DevExpress.Data.v11.1
  • DevExpress.ExpressApp.Images.v11.1
  • DevExpress.ExpressApp.v11.1
  • DevExpress.ExpressApp.Win.v11.1
  • DevExpress.Xpo.v11.1
  • DevExpress.Xpo.v11.1.Providers
  • MySql.Data

Последняя ссылка требует установленного MySQL .NET Connector’а и зависит от той СУБД, которую Вы используете.
Добавим к проекту файл конфигурации и пропишем в нём Connection String до базы данных. Мой конфигурационный файл будет выглядеть так:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="ConnectionString" value="XpoProvider=MySql;server=192.168.1.2;user id=sa; password=123456; database=XafDemo;persist security info=true;CharSet=utf8;"/>
</appSettings>
</configuration>

Модуль бизнес-логики

Теперь нужно создать ещё один проект, в котором будет содержаться бизнес-логика нашего приложения. Этот проект должен быть библиотекой классов. Назовём его DemoModule. Так же, как и в первом случае, выкидываем из вновь созданного модуля всё лишнее. Вот те ссылки, которые должны быть в проекте:

  • System
  • DevExpress.Data.v11.1
  • DevExpress.ExpressApp.v11.1
  • DevExpress.ExpressApp.Win.v11.1
  • DevExpress.Xpo.v11.1

Чтобы XAF заметил наш модуль, мы должны реализовать публичный класс, унаследованный от DevExpress.ExpressApp.ModuleBase. В нашем случае, класс тривиален:

using DevExpress.ExpressApp;

namespace DemoModule
{
public class DemoModule :
ModuleBase
{
}
}

Реализация базового приложения

Теперь у нас есть модуль, который мы можем зарегистрировать в базовом приложении. Возвращаемся в проект XafDemo, добавляем в зависимости проект DemoModule и прописываем его в файле конфигурации

<add key="Modules" value="DemoModule"/>

Для запуска приложения требуется создать экземпляр класса DevExpress.ExpressApp.WinApplication. Обернём создание экземпляра этого класса в класс XafDemoApplication.

using DevExpress.ExpressApp.Win;
using System.Configuration;
using System;
using DevExpress.ExpressApp.Updating;

namespace XafDemo
{
public class XafDemoApplication : IDisposable
{
private WinApplication application;

public XafDemoApplication(string[] arguments)
{
Init();
}

private void Init()
{
application = new WinApplication()
{
Title = "XAF Demo Application"
};
application.Setup("XafDemo", LoadConnectionString(), LoadModules());
}

private string LoadConnectionString()
{
return ConfigurationManager.AppSettings["ConnectionString"];
}

private string[] LoadModules()
{
string modules = ConfigurationManager.AppSettings["Modules"];
if(String.IsNullOrEmpty(modules))
return new string[0];
return modules.Split(';');
}

public void Start()
{
application.Start();
}

public void HandleException(Exception exception)
{
application.HandleException(exception);
}

public void Dispose()
{
application.Dispose();
}
}
}

Метод Setup класса WinApplication, в этом примере, принимает три аргумента: имя приложения (соответствует имени базы данных), connection string к базе данных и загружаемые модули. Метод HandleException показывает удобный Message Box с подробным сообщением об ошибке.
Чтобы запустить приложение, достаточно простой реализации метода Main.

using System;

namespace XafDemo
{
public class XafDemo
{
[STAThread]
public static void Main(string[] args)
{
using(XafDemoApplication application = new XafDemoApplication(args))
{
try
{
application.Start();
}
catch (Exception err)
{
application.HandleException(err);
}
}
}
}
}

Однако, при попытке запустить приложение, окажется, что структура базы данных не созана и приложение завершиться с ошибкой. Для того, чтобы база данных была в актуальном состоянии, её следует постоянно обновлять. Добавим метод для обновления базы в класс XafDemoApplication.

public void UpdateDatabase()
{
DatabaseUpdater updater = application.CreateDatabaseUpdater();
updater.Update();
}

Выполнив этот метод, мы поместим всю служебную информацию в базу. Кроме того, будут созданы таблицы для хранения всех бизнес-объектов, о которых чуть позже.

Database updater

Было бы неосмотрительно давать каждому пользователю возможность обновлять базу, поэтому вынесем функционал для обновления в отдельный проект. Для этого создадим Console Application и, по традиции, выкинем всё лишнее из сгенерированного проекта. Назовём новый проект DBUpdater. Понадобятся нам лишь те библиотеки, которые мы использовали в модуле и сам модуль:

  • System
  • DevExpress.Data.v11.1
  • DevExpress.ExpressApp.v11.1
  • DevExpress.ExpressApp.Win.v11.1
  • DevExpress.Xpo.v11.1 
  • DemoModule

Скопируем файл конфигурации из XafDemo в проект и реализуем метод Main.

using System;
using XafDemo;

namespace DBUpdater
{
public class DBUpdater
{
static void Main(string[] args)
{
using(XafDemoApplication application =
new XafDemoApplication(args))
{
try
{
Console.Write("Updating: ");
application.UpdateDatabase();
Console.WriteLine("success!");
}
catch(Exception err)
{
Console.WriteLine("fail!");
Console.WriteLine("Error details:");
Console.WriteLine("Type: {0}", err.GetType().FullName);
Console.WriteLine("Message: {0}", err.Message);
Console.WriteLine("Stack trace:");
Console.WriteLine(err.StackTrace);
Console.WriteLine();
}
Console.Write("Press any key...");
Console.ReadKey();
}
}
}
}

Запустив это приложение, мы создадим рабочую структуру базы. Но есть одна, не очень приятная, особенность. При выполнении метода Setup XAF показывает splash окно. А в консольном приложении оно выглядит глупо. Добавим в XafDemoApplication функционал для отключения splash. Сделать это можно установив null в свойство SplashScreen класса WinApplication. Добавим конструктор, принимающий флаг того, нужно ли отключать окно приветствия, а в методе Init выполним проверку этого флага и установим null в SplashScreen.

public XafDemoApplication(string[] arguments)
{
Init(false);
}

public XafDemoApplication(string[] arguments, bool suppressSplash)
{
Init(suppressSplash);
}

private void Init(bool suppressSplash)
{
application = new WinApplication()
{
Title = "XAF Demo Application"
};
if(suppressSplash)
application.SplashScreen = null;
application.Setup("XafDemo", LoadConnectionString(), LoadModules());
}

Изменим создание XafDemoApplication в DBUpdater, передав true вторым аргументом и противное окно больше не появится.

Первый запуск

Всё готово к первому запуску. Так как мы не реализовали ни одного бизнес-объекта и не прилинковали ни одного стандартного модуля, то окно приложения будет абсолютно пустым

Бизнес-объекты

Чтобы в нашем окне появилось что-нибудь интересное, нужно это создать. Все бизнес объекты, с которыми работает XAF являются объектами ORM eXpress Persistent Objects (XPO), разрабатываемой той же Developer Express. В качестве базового класса для всех бизнес-объектов удобно использовать класс DevExpress.Xpo.XPObject.
Создадим класс Employee, описывающий сотрудника некой фирмы.

using System;
using DevExpress.Xpo;

namespace DemoModule
{
public class Employee : XPObject
{
private string firstName;
private string secondName;

public Employee(Session session) :
base(session)
{
}

public string FirstName
{
get { return firstName; }
set { SetPropertyValue<string>("FirstName", ref firstName, value); }
}

public string SecondName
{
get { return secondName; }
set { SetPropertyValue<string>("SecondName", ref secondName, value); }
}
}
}

Как видно, нет ничего сложного. Все открытые свойства, по умолчанию, сохраняются в базе и отображаются на форме. Конструктор класса XPObject принимает объект Session, который создаётся при установлении соединения с базой данных. Чтобы поместить новое значение свойства в базу, следует использовать метод SetPropertyValue, объявленный в классе DevExpress.Xpo.PersistentBase.
Чтобы обновить схему в базе данных, запустите DBUpdater.
После запуска XafDemoApplication мы не увидим ни каких изменений. Это случилось потому, что мы не указали XAF, что хотим видеть.

Модель XAF

Для того, чтобы указать XAF, что мы хотим видеть на форме, мы должны создать файл настроек модели. Добавьте простой текстовый файл с именем Model.DesignedDiffs.xafml в проект DemoModule и установите его свойство «Build Action» в «Embedded Resource». Запишите в этот файл следующий текст

<?xml version="1.0" encoding="utf-8"?>
<Application/>

После пересборки проекта этот файл можно использовать для настройки UI. Делается это либо через встроенный в Visual Studio редактор, либо вручную, запустив программу DevExpress.ExpressApp.ModelEditor.v11.1.exe, которая лежит, у меня, по адресу c:Program FilesDevExpress 2011.1eXpressApp FrameworkToolsModel Editor. В качестве опций эта программа принимает путь до модуля (файла *.dll) и путь до каталога с файлом Model.DesignedDiffs.xafml.
Итак, запустив редактор, мы видим следующее окно

Переходим в раздел NavigationItems и добавляем в пункт Items новый NavigationItem, щёлкнув ПКМ. Это будет наш корневой раздел, назовём его «Organization» (впишите имя в поле Id). Добавим во вновь созданный NavigationItem новый NavigationItem и выберем из списка «View» пункт «Employee_ListView». В полях «Id» и «Caption» выставим имя «Employees». В поле ImageName можно выбрать картинку, которая будет отображаться в пункте навигации. Сохраняем настройки и выходим. После прекомпиляции, в нашем окне добавится новая информация.

Нажав на кнопку «new» мы можем создать новый объект класса Employee.

Можно заметить «лишнее» поле «Oid», которое XPO использует для идентификации объектов в базе данных. Естественным желанием будет удалить это поле. Для этого открываем редактор модели и переходим в пункт Views/DemoModule/Employee_DetailView/Layout/Main/SimpleEditors и удаляем пункт XPObject.

Наложение ограничений на свойства

Наша программа позволяет создать объект класса Employee без имени и фамилии. Таких людей в базе мы видеть не хотим. XAF совместно с XPO позволяют наложить ограничения на поля бизнес-классов простым добавлением атрибутов. Чтобы использовать систему валидации нужно добавить две зависимости: DevExpress.Persistent.Base.v11.1 и DevExpress.ExpressApp.Validation.v11.1. В последнем модуле находится XAF модуль DevExpress.ExpressApp.Validation.ValidationModule. Мы должны добавить зависимость от него в наш модуль DemoModule. Для этого в конструктор DemoModule пишем строку

RequiredModuleTypes.Add(typeof(ValidationModule));

Теперь мы можем наложить атрибут DevExpress.Persistent.Validation.RuleRequiredFieldAttribute на поля FirstName и SecondName. Полученный код будет выглядеть так:

[RuleRequiredField(
"RuleRequiredField for DemoModule.Employee.FirstName",
DefaultContexts.Save,
"First Name cannot be empty")]
public string FirstName
{
get { return firstName; }
set { SetPropertyValue<string>("FirstName", ref firstName, value); }
}

[RuleRequiredField(
"RuleRequiredField for DemoModule.Employee.SecondName",
DefaultContexts.Save,
"Second Name cannot be empty")]
public string SecondName
{
get { return secondName; }
set { SetPropertyValue<string>("SecondName", ref secondName, value); }
}

После перекомпиляции проекта и обновления базы при попытке сохранить объект без указания имени и фамилии сотрудника мы получим соответствующую ошибку.

Первым параметром атрибута мы указываем уникальный идентификатор валидатора, вторым аргументом указываем, когда нужно запускать проверку. В последнем параметре указывается сообщение, отображаемое в сообщении об ошибке.
Кроме автоматической валидации, модуль DevExpress.ExpressApp.Validation создаёт кнопку «Validate» в тулбаре окна (зелёная галочка), нажав на которую мы можем принудительно провести валидацию.

Отношение ассоциации «один ко многим»

Очень часто возникает ситуация, когда один объект хранит коллекцию других объектов. Фреймворк XPO позволяет создать такое отношение в базе данных, а XAF обеспечит полноценное отображение и удобную работу с такими ассоциациями.
Давайте создадим класс Position и присвоим каждому сотруднику ссылку на объект этого класса. Каждый сотрудник может занимать только одну должность, в то время, как должность содержит список всех сотрудников, которые её занимают.

using System;
using DevExpress.Xpo;
using DevExpress.Persistent.Validation;

namespace DemoModule
{
public class Position :
XPObject
{
private string name;

public Position(Session session) :
base(session)
{
}

[RuleUniqueValue(
"RuleUniqueValue for DemoModule.Position.Name",
DefaultContexts.Save,
"Name must be unique")]
[RuleRequiredField(
"RuleRequiredField for DemoModule.Position.Name",
DefaultContexts.Save,
"Name cannot be empty")]
public string Name
{
get { return name; }
set { SetPropertyValue<string>("Name", ref name, value); }
}

[Association("Employee-Position")]
public XPCollection<Employee> Employees
{
get { return GetCollection<Employee>("Employees"); }
}
}
}

Обратите внимание, для гарантирования уникальности имени продукта, я применил атрибут RuleUniqueValue.
Для хранения коллекции объектов в базе данных используется тип DevExpress.Xpo.XPCollection. Чтобы реализовать ассоциацию «один ко многим» нужно объявить свойство типа XPCollection, имеющее только getter. Получить коллекцию ассоциированных объектов можно методом GetCollection. Чтобы XPO и XAF знали что с чем ассоциировано нужно добавить свойству атрибут Association с уникальным именем в параметре. Тот же атрибут нужно указать на другом конце ассоциации. Давайте создадим свойство Position в классе Employee.

private Position position;

[Association("Employee-Position")]
[RuleRequiredField(
"RuleRequiredField for DemoModule.Employee.Position",
DefaultContexts.Save,
"Position cannot be empty")]
public Position Position
{
get { return position; }
set { SetPropertyValue<Position>("Position", ref position, value); }
}

С этой стороны ассоциации свойство выглядит так, как и все остальные, за исключением атрибута Association. Откомпилировав приложение, обновив базу, отредактировав модель и запустив программу мы можем создать несколько должностей и сотрудников и сассоциировать их. Примерный результат работы можно видеть на следующем изображении.

Отношение ассоциации «многие ко многим»

Не всегда достаточно отношения «один ко многим». Часто бывает необходимо, чтобы один объект хранил коллекцию других объектов, а другие объекты хранили коллекции первых объектов. XPO и XAF поддерживают и эту концепцию. Для её демонстрации, предположим, что наша организация занимается производством нескольких видов продуктов. Каждый сотрудник может принимать участие в производстве нескольких продуктов, равно как и один продукт производится несколькими сотрудниками. Создадим простой класс Product.

using System;
using DevExpress.Xpo;
using DevExpress.Persistent.Validation;

namespace DemoModule
{
public class Product :
XPObject
{
private string name;

public Product(Session session) :
base(session)
{
}

[RuleUniqueValue(
"RuleUniqueValue for DemoModule.Product.Name",
DefaultContexts.Save,
"Name must be unique")]
[RuleRequiredField(
"RuleRequiredField for DemoModule.Product.Name",
DefaultContexts.Save,
"Namecannot be empty")]
public string Name
{
get { return name; }
set { SetPropertyValue<string>("Name", ref name, value); }
}

[Association("Employee-Product")]
public XPCollection<Employee> Employees
{
get { return GetCollection<Employee>("Employees"); }
}
}
}

Для описания ассоциации «многие ко многим» используется тот же принцип, что и при использовании ассоциации «один ко многим», только на обоих концах ассоциации находятся свойства, возвращающие коллекции.

[Association("Employee-Product")]
public XPCollection<Product> Products
{
get { return GetCollection<Product>("Products"); }
}

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

Пользовательские элементы управления

XAF поддерживает создание пользовательских элементов управления. Для этого нужно реализовать контроллер для одного или нескольких отображений и в нём создать объект одного из наследников класса DevExpress.ExpressApp.Actions.ActionBase. Приведу пример лишь самого простого из них — SimpleAction, который представляет собой кнопку на тулбаре. Наш action будет отображаться в детальном просмотре объектов класса Employee и, при активации, очищать список продуктов сотрудника.

using System;
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Actions;
using DevExpress.Persistent.Base;

namespace DemoModule
{
public class EmployeeController :
ViewController<DetailView>
{
private SimpleAction cleanProductsAction;

public EmployeeController()
{
TargetObjectType = typeof(Employee);
cleanProductsAction = new SimpleAction(this,
"CleanProductsFromEmployee", PredefinedCategory.RecordEdit)
{
Caption = "Clean all products"
};
}

protected override void OnActivated()
{
base.OnActivated();
cleanProductsAction.Execute += OnCleanProductsActionExecute;
}

protected override void OnDeactivated()
{
base.OnDeactivated();
cleanProductsAction.Execute -= OnCleanProductsActionExecute;
}

private void OnCleanProductsActionExecute(object sender,
SimpleActionExecuteEventArgs e)
{
Employee employee = View.CurrentObject as Employee;
if(employee == null)
return;
int count = employee.Products.Count;
for(int i = count - 1; i >= 0; --i)
{
employee.Products.Remove(employee.Products[i]);
}
if(count > 0)
View.ObjectSpace.SetModified(employee);
}
}
}

Для создания контроллера требуется указать тип объекта, на представлении которого он будет активироваться. Первым параметром конструктора класса SimpleAction передаём ссылку на родительский контроллер. Затем передаётся идентификатор action’а и раздел, куда этот action будет помещён.

При активации контроллера вызывается метод OnActivated, в котором мы подписываемся на событие «Execute».
Контроллер содержит в себе ссылку на вид, на котором был активирован в свойстве View. Через этот объект мы можем получить текущий объект, обратившись к свойству CurrentObject. На всякий случай проверим его тип и, перебрав все продукты, удалим их. Изменения в списке XAF не заметит и кнопка «Save» не будет активирована. Для устранения этого эффекта вызываем метод SetModified из пространства объектов текущего вида.

Заключение

В этой статье я рассказал далеко не обо всех возможностях eXpress Application Framework. Чтобы узнать больше, нужно почитать официальную документацию. Возможно, в следующих статьях я расскажу что-то ещё. Скачать полный проект приведённого примера можно отсюда.