Архив рубрики: .NET Framework

Асинхронное программирование на C# в Visual Studio 11

На днях я побывал на конференции Windows 8 Camp и увидел немало интересных вещей. Среди них была продемонстрирована новая концепция асинхронного программирования на C#.
Для написания примеров этой статьи я использовал Visual Studio 11 Developer Preview.
Итак, в C#, входящем в состав Visual Studio 11, появились два новых ключевых слова: async и await. Хотелось бы продемонстрировать их работу сразу на примере.
using System;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncDemo {
class Program {
const string url = "http://download.mozilla.org/?product=firefox-10.0.2&os=win&lang=en-US";
const string filename = "C:\\firefix.exe";

static void Main(string[] args) {
Test();
while(true) {
Console.Write("-");
Console.Out.Flush();
Thread.Sleep(20);
}
}

static async void Test() {
using(WebClient webClient = new WebClient()) {
Task<byte[]> downloadTask = webClient.DownloadDataTaskAsync(url);
byte[] file = await downloadTask;
Console.WriteLine("Downloaded");
using(FileStream stream = File.OpenWrite(filename)) {
Task writeTask = stream.WriteAsync(file, 0, file.Length);
await writeTask;
Console.WriteLine("Written");
}
}
}
}
}
Как видно из примера, кроме новых ключевых слов, в .NET 4.5 появились новые методы. Все методы, поддерживающие асинхронное выполнение, в .NET 4.5 оканчиваются на Async.
Метод, содержащий асинхронное выполнение, должен быть помечен ключевым словом async и делится на две части: до ключевого слова await и после него.
Методы, поддерживающие асинхронное выполнение, должны возвращать значение типа Task или Task<TResult>. Применение операции awiat к объекту Task будет отложено до тех пор, пока задача не будет выполнена. После начала ожидания управление возвращается в рабочий поток, а обработчик будет вызван в другом потоке. Вторая часть метода (после ключевого слова await) и является, по сути, обработчиком события завершения асинхронной операции.
Вывод примера будет следующим:
Как видите, вывод основного потока вклинился между окончанием операции загрузки и завершением сохранения файла.
Нет ничего сложного в написании асинхронных методов самостоятельно. Для этого Вам всего лишь нужно вернуть из Вашего метода значение типа Task или Task<TResult>.  В конструктор такого объекта следует передать делегат, который будет выполняться. Далее, нужно запустить задачу вызовом метода Start и вернуть созданный объект.
using System;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncDemo {
class Program {
static void Main(string[] args) {
Test2();
while(true) {
Console.Write("-");
Console.Out.Flush();
Thread.Sleep(20);
}
}

static async void Test2() {
Task<string> task = new AsyncWorker().WorkAsync();
string message = await task;
Console.WriteLine(message);
}
}

class AsyncWorker {
public Task<string> WorkAsync() {
Task<string> task = new Task<string>(Worker);
task.Start();
return task;
}

string Worker() {
Console.WriteLine("Step 1");
Thread.Sleep(500);
Console.WriteLine("Step 2");
Thread.Sleep(500);
return "Done";
}
}
}
Класс Task содержит некоторое количество интересных статических методов. Ознакомиться с ними можно в документации msdn.

Отслеживание событий тестирования NUnit

Работая с фреймворком для unit-тестирования .NET сборок NUnit, Вы не можете получить доступ к событиям тестирования из Ваших фикстур (Fixture) напрямую. Для отслеживания событий Вы должны написать add-in. В этой статье я приведу простой пример того, как это можно сделать.
Создадим простую библиотеку классов, которую будем тестировать.
using System;

namespace TestedLibrary {
public class Engine {
uint cylinderCount = 1;
float capacity = 0.0f;
float power = 0.0f;

public uint CylinderCount {
get {
return cylinderCount;
}

set {
if(value < 1) {
throw new Exception("Cylinder count must be great then 0");
}
cylinderCount = value;
}
}

public float Capacity {
get {
return capacity;
}

set {
if(value < 0.0) {
throw new Exception("Engine capacity must be great then 0.0");
}
capacity = value;
}
}

public float Power {
get {
return power;
}

set {
if(value < 0.0) {
throw new Exception("Engine power must be great then 0.0");
}
power = value;
}
}
}
}
namespace TestedLibrary {
public class Car {
public Car(string name, Engine engine) {
Name = name;
Engine = engine;
}

public string Name { get; set; }

public Engine Engine { get; set; }
}
}
Создадим проект для тестирования и добавим в него пару фикстур:
using NUnit.Framework;
using TestedLibrary;

namespace Testing {
[TestFixture]
public class EngineTest {
[Test]
public void DefaultValues() {
Engine engine = new Engine();
Assert.AreEqual(0.0f, engine.Power, 0.1f);
Assert.AreEqual(0.0f, engine.Capacity, 0.1f);
Assert.AreEqual(1u, engine.CylinderCount);
}

[Test]
public void ManuallySettedValues() {
Engine engine = new Engine() {
Capacity = 1995.0f,
CylinderCount = 4u,
Power = 135000.0f
};
Assert.AreEqual(135000.0f, engine.Power, 0.1f);
Assert.AreEqual(1995.0f, engine.Capacity, 0.1f);
Assert.AreEqual(4u, engine.CylinderCount);
}
}
}
using NUnit.Framework;
using TestedLibrary;

namespace Testing {
[TestFixture]
public class CarTest {
[Test]
public void Creation() {
Engine engine = new Engine() {
Capacity = 1995.0f,
CylinderCount = 4u,
Power = 135000.0f
};
Car car = new Car("BMW Х3 xDrive20d Urban", engine);
Assert.AreSame(engine, car.Engine);
Assert.AreEqual("BMW Х3 xDrive20d Urban", car.Name);
}
}
}
Теперь можем скомпилировать и запустить unit-тесты для нашей библиотеки
Как я уже говорил, чтобы добраться до событий тестирования, нужно создать add-in для NUnit. Для этого нужно создать новый проект C# из шаблона "Class Library" и добавить в него ссылки на сборки nunit.core.dll и nunit.core.interfaces.dll. По умолчанию они находятся в каталоге C:Program Files (x86)NUnit 2.5.10binnet-2.0lib.
При запуске NUnit просматривает каталог addins, находящийся рядом с исполняемым файлом, и загружает из всех сборок классы с атрибутом NUnit.Core.Extensibility.NUnitAddinAttribute. Кроме того, эти классы должны реализовывать интерфейс NUnit.Core.Extensibility.IAddin, содержащий всего один метод:
namespace NUnit.Core.Extensibility {
public interface IAddin {
bool Install(IExtensionHost host);
}
}
Для того, чтобы подписаться на прослушивание событий, мы должны реализовать интерфейс NUnit.Core.EventListener и передать объект этого интерфейса в метод Install объекта типа NUnit.Core.Extensibility.IExtensionPoint, который нужно получить следующим образом:
IExtensionPoint listeners = host.GetExtensionPoint("EventListeners");
Наш класс прослушивания будет называться NUnitReporter и его целью будет создание отчёта в формате HTML. В качестве параметра конструктор этого класса будет принемать имя выходного файл. С учётом всего выше сказанного, полный код класса-расширения будет следующим:
using NUnit.Core.Extensibility;

namespace NUnitReport {
[NUnitAddin]
public class Addin : IAddin {
public bool Install(IExtensionHost host) {
IExtensionPoint listeners = host.GetExtensionPoint("EventListeners");
listeners.Install(new NUnitReporter(@"D:report.html"));
return true;
}
}
}
Интерфейс NUnit.Core.EventListener выглядит следующим образом:
namespace NUnit.Core {
public interface EventListener {
void RunFinished(Exception exception);
void RunFinished(TestResult result);
void RunStarted(string name, int testCount);
void SuiteFinished(TestResult result);
void SuiteStarted(TestName testName);
void TestFinished(TestResult result);
void TestOutput(TestOutput testOutput);
void TestStarted(TestName testName);
void UnhandledException(Exception exception);
}
}
Методы RunStarted и RunFinished вызываются при старте и завершении тестирования. В качестве аргумента методу RunFinished передаются результаты всего тестирования или объект возникшего исключения. SuiteStarted и SuiteFinished вызываются для всех сущностей шире единичного теста: класс, пространство имён, Run. Методы TestStarted и TestFinished будут вызваны при запуске и завершении каждого теста. Наконец, метод UnhendledException вызывается, как ясно из названия, при возникновении необработанного исключения.
Приведу простейшую реализацию класса NUnitReporter, собирающего информацию в файл HTML.
using System;
using System.Collections;
using System.IO;
using System.Text;
using NUnit.Core;

namespace NUnitReport {
public class NUnitReporter : EventListener {
string outFileName;
StringBuilder html;

public NUnitReporter(string outFileName) {
this.outFileName = outFileName;
}

public void RunFinished(System.Exception exception) {
}

public void RunFinished(TestResult result) {
PrintRunResult(result);
using(FileStream stream = File.OpenWrite(outFileName)) {
byte[] content = Encoding.UTF8.GetBytes(html.ToString());
stream.Write(content, 0, content.Length);
}
}

void PrintRunResult(TestResult result) {
html = new StringBuilder(
"<html>rn" +
"<head>rn" +
"<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>rn" +
"<title>NUnit Report</title>rn" +
"</head>rn" +
"<body>rn" +
"<div id='run'>rn");
html.AppendLine(String.Format("<h1>{0}</h1>", result.Name));
PrintSuiteResults(result.Results);
html.AppendLine(
"</div>rn" +
"</body>rn" +
"</html>");
}

void PrintSuiteResults(IList results) {
foreach(TestResult result in results) {
html.AppendLine("<div class='suite'>");
html.AppendLine(String.Format("<h2>{0}</h2>", result.Name));
PrintClassResults(result.Results);
html.AppendLine("</div>");
}
}

void PrintClassResults(IList results) {
foreach(TestResult result in results) {
html.AppendLine("<div class='class'>");
html.AppendLine(String.Format("<h3>{0}</h3>", result.Name));
PrintTestResults(result.Results);
html.AppendLine("</div>");
}
}

void PrintTestResults(IList results) {
foreach(TestResult result in results) {
string stringResult = "";
if(result.IsSuccess) stringResult = "Success";
else if(result.IsFailure) stringResult = "Failure";
else if(result.IsError) stringResult = "Error";
html.AppendLine("<div class='test'>");
html.AppendLine(String.Format("<p>{0}: {1}</p>", result.Name, stringResult));
html.AppendLine("</div>");
}
}

public void RunStarted(string name, int testCount) {
}

public void SuiteFinished(TestResult result) {
}

public void SuiteStarted(TestName testName) {
}

public void TestFinished(TestResult result) {
}

public void TestOutput(TestOutput testOutput) {
}

public void TestStarted(TestName testName) {
}

public void UnhandledException(System.Exception exception) {
}
}
}
Больший интерес во всём примере представляет объект класса NUnit.Core.TestResult. Этот объект содержит информацию о результатах только что завершившегося теста и всех вложенных. К примеру, в результат тестирования пространства имён будут вложены результаты тестирования всех его классов. Эти результаты помещаются в список NUnit.Core.TestResult.Results.
Кроме того, класс NUnit.Core.TestResult имеет множетсво интересных свойств:
public int AssertCount { get; set; }
public string Description { get; }
public bool Executed { get; }
public FailureSite FailureSite { get; }
public virtual string FullName { get; }
public bool HasResults { get; }
public virtual bool IsError { get; }
public virtual bool IsFailure { get; }
public virtual bool IsSuccess { get; }
public string Message { get; }
public virtual string Name { get; }
public IList Results { get; }
public ResultState ResultState { get; }
public virtual string StackTrace { get; set; }
public ITest Test { get; }
public double Time { get; set; }
Файл, содержащий класс Addin следует положить в каталог addins, рядом с исполняемым файлом nunit.exe. Если такого каталога не существует, его нужно создать. Для отладки библиотеки следует аттачиться к процессу nunit-agent.exe (Debug->Attach to Process).
Полученный отчёт будет выгладить так:
Естественно, что приведённый пример сильно упрощён и не годится для реального использования, но, я надеюсь, демонстрирует общий подход.
Исходные тексты проекта можно скачать здесь.

Краткое введение в eXpress Application Framework

Часто тут и там мы слышим о концепции отделения бизнес-логики приложения от UI. Разные софтверные компании предлагают разные решения этой задачи. Microsoft, к примеру, продвигает технологию WPF, а для Qt разрабатывается QML. В этой статье я хочу познакомит Вас с решением от компании Developer Express -- eXpress 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. Чтобы узнать больше, нужно почитать официальную документацию. Возможно, в следующих статьях я расскажу что-то ещё. Скачать полный проект приведённого примера можно отсюда.

C#, кто он?

Не так давно я начал осваивать нынче сверхпопулярный, среди windows программистов, язык программирования C#. Осваивать я его начал, естественно, не просто так, а с корыстными целями, но об этом я рассказывать не буду. Нельзя сказать, что до этого я его в глаза не видел, писал я маленький тестик. Теперь же, после недельного знакомства с этим языком, я хочу поделиться своими впечатлениями.Сразу хочу предупредить, что всё, написанное в этой статье, является моим субъективным мнением.

Платформа .NET

Для работы программ, созданных на платформе .NET, то есть всех программ на языке C#, требуется виртуальная машина .NET. Платформа .NET должна была создать конкуренцию платформе Java, но разработчики Microsoft остановились на поддержке только операционных систем семейства Windows, похоронив тем самым кроссплатформенность. Однако компания Novell разрабатывает альтернативную кроссплатформенную реализацию .NET и C#. Этот проект получил название Mono.
Даже несмотря на то, что Microsoft предоставила Novell все права, проект Mono остаётся догоняющим. По этой причине проектирование Mono сильно отстаёт. Например, WPF в проекте Mono даже и не собираются реализовывать.Исходя из вышесказанного, нетрудно предположить, что мы можем либо писать только под Windows, либо принять ограничения Mono и жить в их рамках.
Но не всё так плохо, того, что реализовано проектом Mono достаточно для создания полноценных приложений. Открытым остаётся вопрос, а согласятся ли компании, создающие ПО на C#, на такие ограничения?
Проект .NET создавался сразу с несколькими целями, давайте разберёмся, что это за цели и каков результат.

Замена устаревшим технологиям COM и ActiveX

Позволю себе напомнить о том, что такое COM и ActiveX. Технология COM служит для межпроцессного, внутрепроцессного и сетевого взаимодействия объектно-ориентированных модулей. Взаимодействие строится на понятиях виртуальной таблицы (VTBL) и интерфейса класса. Приложение, которое хочет воспользоваться объектом другого модуля, просит этот модуль создать ему экземпляр какого-либо класса и, получив его, инициализирует специальным образом, заранее известный, интерфейс. Системой маршалинга COM вызовы виртуальных методов интерфейса преобразуются в корректные вызовы и все остаются довольны. Технология ActiveX - это надстройка над COM, которая служит для работы с графическим интерфейсом, обеспечивая такие понятия как компонент, узел компонента и обмен сообщениями.
Итак, как же технология .NET решает те же проблемы, что и COM. Во-первых .NET вводит понятие сборки (assembly), которая является её функциональной частью. Сборка представляет из себя бинарный модуль, хранящий полную информацию о том, что он содержит: пространства имён, зависимости, типы, методы итд. Любая сущность, объявленная в сборке как public автоматически становится доступной из других сборок. При том все события и делегаты остаются в рабочем состоянии, а виртуальная машина .NET обеспечивает, как и в случае с COM, все межпроцессные, внутрипроцессные и сетевые взаимодействия. Таким образом, отпадает необходимость в виртуальных таблицах, а значит и работа с графическими компонентами становится неотличимой от простого взаимодействия.
С первой задачей .NET справился, но на этом создатели не остановились. Раз уж нам известно всё о типах, находящихся внутри сборки, то пользователь этой сборки может расширять все типы, унаследовав их. Технология COM не позволяет наследоваться от классов из скомпилированного модуля.

Мультиязычная поддержка

В рамках платформы .NET реализована технология под названием CLR (Common Language Runtime). Это технология, позволяющая работать с платформой .NET из других языков программирования. Также, предоставляется возможность взаимодействия сборок, написанных на разных языках.
В рамках технологии CLR реализовано множество языков, как самой корпорацией Microsoft, так и другими компаниями. Среди этих языков, такие языки как C#, Visual Basic.NET, C++.NET, F#, IronPython, Delphi, Nemerle и многие другие. Так как я не работал со многими из тех языков, что значатся в списке, поддерживающих CLR, я скажу только о C++.NET. Достаточно справедливо будет отметить, что разработчики компании Microsoft внесли такие изменения в язык C++, что работать с ним стало просто не приятно. Попробовав поработать с C++.NET, я понял, что не выдержу издевательств над своим любимым языком и поспешно удалил тестовый проект. Больше я к нему не возвращался. Получается, что не CLR поддерживает язык, а язык изменили для CLR (то есть подвинули гору к Магомету). Из этого можно сделать вывод, что некоторые языки, как минимум C++ и Visual Basic подгонялись под платформу .NET только для того, чтобы создать изначальный пул языков CLR. Надеюсь, что с другими языками, которые создавались не в такой спешке, подобной истории не случилось.
Подводя итоги для этой цели, можно сказать, что платформа .NET не полностью справилась с поставленной задачей, так как я вынужден использовать не C++, а его модификацию только потому, что разработчикам .NET так захотелось. Но нельзя отрицать тот факт, что поддерживаемых языков, действительно, много, и их количество возрастает.

Язык C#

Давайте же перейдём к виновнику сей статьи - к языку C#. Язык C# - это первый из языков CLR, который создавался с нуля и специально для платформы .NET, поэтому он должен отражать все намерения разработчиков. Язык разрабатывался под сильным влиянием языка Java и должен был стать его конкурентом. В этой статье я не буду писать учебник по этому языку, о нём написано много книг и на msdn есть руководство. Здесь я расскажу о тех конструкциях языка, которые мне понравились и о тех, которые мне не понравились.

Наследование

Как и в Java, в C# запрещено множественное наследование классов, но разрешено множественное наследование интерфейсов. Хорошо это или плохо? Споры по этому поводу не умолкают с тех пор, как придумали наследование. Моё мнение - плохо. Ведь у каждого программиста есть своя собственная голова на плечах и он ею может подумать и сам решить все проблемы, которые несёт неверное использование множественного наследования, а современные компиляторы способны подсказать ему, что в коде возникла путаница. А вот пользы от множественного наследования гораздо больше, чем вреда. Если вспомнить истоки ООП, то основной причиной создания этого подхода было определение типов, соответствующих реальным объектам и предметам. А коли это так, то "диван-кровать" и "компьютер моноблок" являются примерами таких реальных объектов, которые в ООП было бы правильно описывать множественным наследованием (от "дивана" и "кровати" в первом случае и от "системный блок" и "монитор" - во втором).

Упаковка объектов

В C# все типы неявно наследуются от типа System.Object или просто object. Тип object используется для, так называемой, упаковки объектов. Упаковка в синтаксическом виде - это ни что иное, как приведение объекта к типу object
string str = "Hello";
object obj = (object)str;
Надо отметить, что не смотря на все советы Скотта Мэйерса, приведение типов в C# осталось в стиле языка Си.
Итак, что же особенного в данной ситуации. Упаковка объектов в платформе .NET используется повсеместно, что может привести к путанице. Конечно, при неверном приведении среда исполнения выдаст исключение, но мне представляется, что runtime - это уже поздно. Ловить исключения при каждом присвоении смешно, согласитесь.
В качестве примера использования упаковки можно привести операцию клонирования. В интерфейсе IClonable объявлен метод Clone, который возвращает тип object. Служит этот метод только одной цели - создание копии объекта. Давайте посмотрим, на то, как выглядит определение и использование клонирования.
class Sheep : ICloneable
{
private string theName;
private int theAge;

public Sheep(string aName, int anAge)
{
theName = aName;
theAge = anAge;
}

public object Clone()
{
return new Sheep(theName, theAge);
}
}

class Program
{
static void Main(string[] args)
{
Sheep dolly = new Sheep("Dolly", 1);
Sheep dolly2 = (Sheep)dolly.Clone();
}
}
Представляется мне это всё, мягко говоря, не удобным и плохо читабельным.

Копирование объектов

Раз уж я заговорил о клонировании, то добью эту тему до конца. В C# все типы данных поделены на два вида: структурные и ссылочные. Структурные - это те типы, которые мы, C++ программисты, привыкли называть POD (Primitive Old Data) типами (int, float, bool, ...), перчисления и структуры - которые в C# обрели новую жизнь. Все структурные типы являются наследниками типа System.ValueType и память для их объектов всегда выделяется в стеке. Все остальные типы, то бишь классы и интерфейсы, называются ссылочными и наследуются, как уже было сказано, от System.Object или любого производного, кроме System.ValueType. Память для объектов ссылочных типов выделяется в управляемой куче.
До сих пор всё выглядит сносно, но теперь давайте представим всё на деле. Допустим, у нас есть некая структура, содержащая какие-то данные.
struct Data
{
public int theData;

public Data(int aData)
{
theData = aData;
}
}

class Program
{
static void PrintIncrementedData(Data data)
{
Console.Write("Incremental data: {0}n", ++data.theData);
}

static void Main(string[] args)
{
Data data = new Data(50);
PrintIncrementedData(data);
PrintIncrementedData(data);
}
}
Данная программа не выглядит реально, но тем не менее, ведёт себя так, как мне хочется, а именно, выводит такой результат
Incremental data: 51
Incremental data: 51
Теперь давайте представим, что тот, кто сопровождает эту структуру неожиданно переименует её в класс, ну, допустим, ему понадобится конструктор по умолчанию, который запрещено создавать в структурах C#. Тогда вывод нашей программы внезапно приобретает другой вид
Incremental data: 51
Incremental data: 52
Одно слово и наша программа перестала работать так, как положено, но продолжила компилироваться без ошибок и предупреждений. Всё это произошло по одной причине: структурные типы при присваивании и передачи в качестве параметров копируются по значению, а ссылочные, как видно из названия, - копируют только ссылку на объект в памяти.
Не знаю, какой тайный смысл разработчики C# вложили в структуры, но трудно обнаруживаемые ошибки, связанные с их использованием представляются более, чем вероятными.

Константность

Здесь я хотел бы поговорить о таких понятиях, которые с C++ называются константными методами и константными возвращаемыми значениями. Если быть точнее, то возвращаемое значение просто имеет константный тип. Но формулировки не столь важны, в C# нет ни того, ни другого. Методы в C# всегда возвращают ссылку на объет ссылочного типа или копию объекта структурного типа. Чем это чревато. Давайте посмотрим на пример из книги "C# и платформа .NET" Эндрю Троелсена, описанный в третьей главе, в разделе, посвящённом инкапсуляции. Так как этот пример там размазан по нескольким параграфам, я соберу его и немного дополню, смысл от этого не изменится.
class FullName
{
public string firstName;
public string secondName;

public FullName(string aFirstName, string aSecondName)
{
firstName = aFirstName;
secondName = aSecondName;
}
}

class Emploee
{
private FullName theFullName;

public Emploee(FullName aFullName)
{
theFullName = aFullName;
}

public FullName Name
{
get { return theFullName; }
}
}


class Program
{
static void Main(string[] args)
{
Emploee emploee = new Emploee(new FullName("William", "Gates"));
FullName name = emploee.Name;
name.firstName = "Linus";
name.secondName = "Torvalds";
Console.Write("1: {0} {1}n", emploee.Name.firstName,
emploee.Name.secondName);
Console.Write("2: {0} {1}n", name.firstName, name.secondName);
}
}
Не смотря на то, что автор утверждает, что свойство Name доступно только для чтения, я не применяя никаких усилий изменил значение приватной переменной, в чём легко убедиться, скомпилировав и запустив программу.
1: Linus Torvalds
2: Linus Torvalds
Стоит ли упоминать, что подобное поведение может привести к, довольно плачевным, последствиям. Для того, чтобы избежать такой ситуации не остаётся ничего другого, кроме как скопировать объект FullName перед возвратом из свойства.
public FullName Name
{
get { return new FullName(theFullName.firstName,
theFullName.secondName); }
}
Естественно, такой подход гораздо более затратный, нежели возврат константной ссылки в C++.

Делегаты

Увидев в первый раз концепцию делегатов, я пришёл в восторг. Это, действительно, мощная и простая технология позволяет создавать такие конструкции, для которых в C++ применялись колбеки, функторы и обсерверы. Если вкратце, то делегат - это особый вид функции, который объявляется в одном классе, а реализован может быть в другом. Кроме того, один делегат может быть реализован много раз и, при вызове, такого делегата, будут вызваны все его реализации. Давайте посмотрим на примере.
class People
{
public delegate void SpeakDelegate();
public SpeakDelegate Speak;
}

class Man
{
private string theName;

public Man(string aName)
{
theName = aName;
}

public void Speak()
{
Console.Write("Hello, my name is {0}n", theName);
}
}

class Program
{
static void Main(string[] args)
{
Man jhon = new Man("Jhon");
Man alice = new Man("Alice");
People people = new People();
people.Speak +=
new People.SpeakDelegate(jhon.Speak) +
new People.SpeakDelegate(alice.Speak);
people.Speak();
}
}
При вызове people.Speak будут вызваны методы Speak двух объектов - jhon и alice, а результат будет таким
Hello, my name is Jhon
Hello, my name is Alice
Этот же механизм (с небольшими дополнениями) используется при отправке сообщений. Но об этом я говорить не буду.
В общем-то, всё это выглядит очень красиво, но стоит помнить одну деталь - вызов делегата, у которого нет подписчиков приведёт к генерации исключения и крешу программы, если его не обработать. Следовательно, при вызове делегата мы должны каждый раз проверять, инициализирован ли он, благо не инициализированные делегаты всегда равны null. На первый взгляд, может показаться, что разработчики могли бы гарантировать безопасный вызов делегатов. Но нет, делегаты могут возвращать значение, а какое значение может вернуть то, что не было вызвано? Раз уж я об этом заговорил, то возвращаемым значением делегата - это значение, которое вернёт последняя вызванная делегатом функция.

Оператор foreach

Конечно же, каждый программист на C++ мечтает увидеть в своём любимом языке оператор обхода коллекций foreach. В языке C# он существует. При том синтаксис этого оператора весьма удобен, а работать с ним одно удовольствие.
foreach(ТипЭлементаКоллекции ссылка in коллекция)
{
}
"ссылка" принимает значение каждого из элементов и является именно ссылкой, а не копией. Чтобы Ваш класс мог работать как коллекция в операторе foreach, он должен реализовать интерфейс IEnumerable, в котором всего один метод - GetEnumerator. Вот если Вы используете не встроенные коллекции, то Вам придётся реализовать и интерфейс IEnumerator.

Коллекции

Список всевозможных коллекций в .NET достаточно большой, но что более всего меня вводит в недоумение - это то, что большинство из них не являются обобщёнными. Например, класс ArrayList хранит объекты типа object. Как я уже говорил в разделе об упаковке объектов, это может вызвать путаницу. По какой причине часть контейнеров сделали обобщёнными, а часть - хранящими объекты типа object для меня остаётся загадкой.

Разные мелочи

Из того, что я видел только в C#, стоит отметить статические конструкторы. Это особый вид конструкторов, в котором можно инициализировать статические члены класса. Кроме того, статические члены можно инициализировать прямо при объявлении, даже вызовом какой-либо функции.
Не удалось мне найти достойной замены оператору typedef из C++. Можно использовать ключевое слово using, но распространятся такой "typedef" будет только внутри одного модуля.

Итоги

Подводя итоги, могу сказать следующее: C# - довольно неплохой язык, но в некоторых местах, явно, плохо продуманный. Обойти все изъяны можно, но лучше бы разработчики подумали о них во время проектирования.