вторник, 26 ноября 2013 г.

StarBan v.0.1 preview

StarBan - методология разработки, которая подходит для крупных команд, делающих один или несколько немасштабных проектов в рамках одного product backlog.
Старбан позволяет менеджеру гибко управлять приоритетами в бэклоге прозрачно для программистов. Эта методология, вобравшая в себя самый приятный опыт скрама и канбана, позволяет за счёт геймификации процесса разработки ПО упростить управление проектом и дать программистам нематериальный стимул к работе.
Старбан, рожденный в недрах РИКРОПа, показал свою эффективность для команд от 6 до 10 человек, но мы надеемся опробовать его в еще более крупномасштабной разработке. Вкратце можно объяснить эту практику следующим образом:

четверг, 24 октября 2013 г.

Фреймворк: Rikrop.Core.Wpf.dll. Часть 0, INotifyPropertyChanged

К оглавлению

 Rikrop.Core.Wpf.dll

Библиотека содержит огромное количество полезных инструментов для быстрого создания отзывчивых и умных wpf-приложений.
Мы любим создавать wpf-приложения. И мы делаем это так часто, что просто не смогли обойтись без полной целостной инфраструктуры для поддержания паттерна mvvm. Мы пробовали использовать разные подходы: view-first, viewmodel-first. И очень важной целью было создание такого набора классов, который позволял бы с минимальными затратами решать типичные для этих подходов задачи.
Мы глубоко интегрировали работу с деревьями выражений для упрощения работы с INotifyPropertyChanged. Для навигации и управления представлением используется абстракция рабочих областей, которая является центральной при построении отображения в wpf.

namespace Rikrop.Core.Wpf

Для удобной, типобезопасной и прозрачной работы с классами, реализующими интерфейс INotifyPropertyChanged, мы создали базовую реализацию ChangeNotifier, интерфейс которого выглядит следующим образом:

[DataContract(IsReference = true)]
[Serializable]
public abstract class ChangeNotifier : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = "")

    protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
    protected void NotifyPropertyChanged(Expression<Func<object, object>> property)
    protected void NotifyPropertyChanged(Expression<Func<object>> property)
    protected virtual void OnPropertyChanged(string propertyName)

    protected ILinkedPropertyChanged AfterNotify(Expression<Func<object> property)
    protected ILinkedPropertyChanged BeforeNotify(Expression<Func<object>> property)
    protected ILinkedPropertyChanged AfterNotify<T>(T changeNotifier, Expression<Func<T, object>> property)
        where T : INotifyPropertyChanged
    protected ILinkedPropertyChanged BeforeNotify<T>(T changeNotifier, Expression<Func<T, object>> property)
        where T : ChangeNotifier

    protected ILinkedObjectChanged Notify(Expression<Func<object>> property)
}

Здесь же сразу стоит указать интерфейсы ILinkedPropertyChanged и ILinkedObjectChanged:
public interface ILinkedPropertyChanged
{
    ILinkedPropertyChanged Notify(Expression<Func<object>> targetProperty);
    ILinkedPropertyChanged Execute(Action action);
}

public interface ILinkedObjectChanged
{
    ILinkedObjectChanged AfterNotify(Expression<Func<object>> sourceProperty);
    ILinkedObjectChanged AfterNotify<T>(T sourceChangeNotifier, Expression<Func<T, object>> sourceProperty)
            where T : INotifyPropertyChanged;

    ILinkedObjectChanged BeforeNotify(Expression<Func<object>> sourceProperty);
    ILinkedObjectChanged BeforeNotify<T>(T sourceChangeNotifier, Expression<Func<T, object>> sourceProperty)
            where T : ChangeNotifier;
}

Как и зачастую в программировании - реализация проще, чем может показаться на первый взгляд, а использование еще проще, чем реализация. Если попытаться описать назначение методов ChangeNotifier "на пальцах", то этот класс позволяет информировать об изменениях свойств с помощью деревьев выражений, а так же указывать взаимосвязи между свойствами таким же образом. Для повышения производительности мы кэшируем деревья выражений и при повторном использовании применяем уже разобранные деревья.*
Но что может быть понятнее простого практического примера?! Следующий код иллюстрирует устройство с несколькими датчиками, где каждый датчик имеет значение и известное ему отклонение от среднего значения, а устройство управляет обновлением данных:
/// <summary>
/// Датчик.
/// </summary>
public class Sensor : ChangeNotifier
{
    /// <summary>
    /// Значение измерения.
    /// </summary>
    public int Value
    {
        get { return _value; }
        set { SetProperty(ref _value, value); }
    }
    private int _value;

    /// <summary>
    /// Отклонения значения измерения от среднего.
    /// </summary>
    public double Delta
    {
        get { return _delta; }
        set { SetProperty(ref _delta, value); }
    }
    private double _delta;

    public Sensor()
    {
        IValueProvider valueProvider = new RandomValueProvider();
        Value = valueProvider.GetValue(this);
    }
}

/// <summary>
/// Прибор с датчиками, проводящими измерения.
/// </summary>
public class Device : ChangeNotifier
{
    /// <summary>
    /// Число датчиков.
    /// </summary>
    private const int SensorsCount = 3;

    /// <summary>
    /// Множество датчиков в устройстве.
    /// </summary>
    public IReadOnlyCollection<Sensor> Sensors
    {
        get { return _sensors; }
    }
    private IReadOnlyCollection<Sensor> _sensors;
        
    /// <summary>
    /// Среднее значение с датчиков.
    /// </summary>
    public double AvgValue
    {
        get { return (Sensors.Sum(s => s.Value)) / (double) Sensors.Count; }
    }

    public Device()
    {
        InitSensors();

        AfterNotify(() => AvgValue).Execute(UpdateDelta);

        NotifyPropertyChanged(() => AvgValue);
    }

    private void InitSensors()
    {
        var sensors = new List<Sensor>();
        for (int i = 0; i < SensorsCount; i++)
        {
            var sensor = new Sensor();
            BeforeNotify(sensor, s => s.Value).Notify(() => AvgValue);
            sensors.Add(sensor);
        }

        _sensors = sensors;
    }

    private void UpdateDelta()
    {
        foreach (var sensor in Sensors)
            sensor.Delta = GetDelta(sensor);
    }

    private double GetDelta(Sensor sensor)
    {
        return Math.Abs(sensor.Value - AvgValue);
    }
}

Интересующие нас строки кода:
SetProperty(ref _delta, value);
NotifyPropertyChanged(() => AvgValue);
AfterNotify(() => AvgValue).Execute(UpdateDelta);
BeforeNotify(sensor, s => s.Value).Notify(() => AvgValue);

Отдельно разберем каждую конструкцию и посмотрим на реализацию приведенных методов.

SetProperty(ref _delta, value);

Этот код присваивает полю, переданному в первом параметре метода, значение из второго параметра, а так же уведомляет подписчиков об изменении свойства, имя которого передаётся третьим параметров. Если третий параметр не задан, используется имя вызывающего свойства.
protected void SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
{
    if (Equals(field, value))
    {
        return;
    }

    field = value;
    NotifyPropertyChangedInternal(propertyName);
}

NotifyPropertyChanged(() => AvgValue);

Все методы нотификации об изменении объектов, принимают ли они дерево выражений или строковое значение имени свойства, в конечном итоге вызывают следующий метод:
private void NotifyPropertyChanged(PropertyChangedEventHandler handler, string propertyName)
{
    NotifyLinkedPropertyListeners(propertyName, BeforeChangeLinkedChangeNotifierProperties);

    if (handler != null)
    {
        handler(this, new PropertyChangedEventArgs(propertyName));
    }
    OnPropertyChanged(propertyName);

    NotifyLinkedPropertyListeners(propertyName, AfterChangeLinkedChangeNotifierProperties);
}

private void NotifyLinkedPropertyListeners(string propertyName,
                                            Dictionary<string, LinkedPropertyChangeNotifierListeners> linkedChangeNotifiers)
{
    LinkedPropertyChangeNotifierListeners changeNotifierListeners;
    if (linkedChangeNotifiers.TryGetValue(propertyName, out changeNotifierListeners))
    {
        changeNotifierListeners.NotifyAll();
    }
}

Каждый объект-наследник ChangeNotifier хранит коллекции связок "имя свойства" - "набор слушателей уведомлений об изменении свойств":
private Dictionary<string, LinkedPropertyChangeNotifierListeners> AfterChangeLinkedChangeNotifierProperties { get { ... } }
private Dictionary<string, LinkedPropertyChangeNotifierListeners> BeforeChangeLinkedChangeNotifierProperties { get { ... } }

Отдельно необходимо рассмотреть класс LinkedPropertyChangeNotifierListeners:
private class LinkedPropertyChangeNotifierListeners
{
    /// <summary>
    /// Коллекция пар "связанный объект" - "набор действий над объектом"
    /// </summary>
    private readonly Dictionary<ChangeNotifier, OnNotifyExecuties> _linkedObjects =
        new Dictionary<ChangeNotifier, OnNotifyExecuties>();

    /// <summary>
    /// Регистрация нового связанного объекта.
    /// </summary>
    /// <param name="linkedObject">Связанный объект.</param>
    /// <param name="targetPropertyName">Имя свойства связанного объекта для уведомления.</param>
    public void Register(ChangeNotifier linkedObject, string targetPropertyName)
    {
        var executies = GetOrCreateExecuties(linkedObject);

        if (!executies.ProprtiesToNotify.Contains(targetPropertyName))
        {
            executies.ProprtiesToNotify.Add(targetPropertyName);
        }
    }

    /// <summary>
    /// Регистрация нового связанного объекта.
    /// </summary>
    /// <param name="linkedObject">Связанный объект.</param>
    /// <param name="action">Действие для вызова.</param>
    public void Register(ChangeNotifier linkedObject, Action action)
    {
        var executies = GetOrCreateExecuties(linkedObject);

        if (!executies.ActionsToExecute.Contains(action))
        {
            executies.ActionsToExecute.Add(action);
        }
    }

    /// <summary>
    /// Получение имеющегося или создание нового набора действий над связанным объектом.
    /// </summary>
    /// <param name="linkedObject">Связанный объект.</param>
    /// <returns>Обёртка над набором действий со связанным объектом.</returns>
    private OnNotifyExecuties GetOrCreateExecuties(ChangeNotifier linkedObject)
    {
        OnNotifyExecuties executies;
        if (!_linkedObjects.TryGetValue(linkedObject, out executies))
        {
            executies = new OnNotifyExecuties();
            _linkedObjects.Add(linkedObject, executies);
        }
        return executies;
    }

    /// <summary>
    /// Вызов уведомлений и действий для всех связанных объектоы.
    /// </summary>
    public void NotifyAll()
    {
        foreach (var linkedObject in _linkedObjects)
        {
            NotifyProperties(linkedObject.Key, linkedObject.Value.ProprtiesToNotify);
            ExecuteActions(linkedObject.Value.ActionsToExecute);
        }
    }

    /// <summary>
    /// Вызов уведомлений об изменении свойств над связанным объектом.
    /// </summary>
    /// <param name="linkedObject">Связанный объект.</param>
    /// <param name="properties">Имена свойств связанного объекта для уведомления.</param>
    private void NotifyProperties(ChangeNotifier linkedObject, IEnumerable<string> properties)
    {
        foreach (var targetProperty in properties)
        {
            linkedObject.NotifyPropertyChangedInternal(targetProperty);
        }
    }

    /// <summary>
    /// Вызов действий.
    /// </summary>
    /// <param name="actions">Действия</param>
    private void ExecuteActions(IEnumerable<Action> actions)
    {
        foreach (var action in actions)
        {
            action();
        }
    }

    private class OnNotifyExecuties
    {
        private List<string> _proprtiesToNotify;
        private List<Action> _actionsToExecute;

        public List<string> ProprtiesToNotify
        {
            get { return _proprtiesToNotify ?? (_proprtiesToNotify = new List<string>()); }
        }

        public List<Action> ActionsToExecute
        {
            get { return _actionsToExecute ?? (_actionsToExecute = new List<Action>()); }
        }
    }
}

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

AfterNotify(() => AvgValue).Execute(UpdateDelta);

BeforeNotify(sensor, s => s.Value).Notify(() => AvgValue);

Для добавления нового связанного объекта и действий над ним служат связка методов AfterNotify/BeforeNotify класса ChangeNotifier и методов Notify/Execute классов-наследников ILinkedPropertyChanged. В качестве последних выступают вложенные по отношению к ChangeNotifier классы AfterLinkedPropertyChanged и BeforeLinkedPropertyChanged.
/// <summary>
/// Связыватель для событий перед нотификаций об изменении свойства объекта.
/// </summary>
private class BeforeLinkedPropertyChanged : ILinkedPropertyChanged
{
    /// <summary>
    /// Исходный объект.
    /// </summary>
    private readonly ChangeNotifier _sourceChangeNotifier;

    /// <summary>
    /// Имя свойство исходного объекта.
    /// </summary>
    private readonly string _sourceProperty;

    /// <summary>
    /// Связываемый объект.
    /// </summary>
    private readonly ChangeNotifier _targetChangeNotifier;

    public BeforeLinkedPropertyChanged(ChangeNotifier sourceChangeNotifier,
                                        string sourceProperty,
                                        ChangeNotifier targetChangeNotifier)
    {
        _sourceChangeNotifier = sourceChangeNotifier;
        _sourceProperty = sourceProperty;
        _targetChangeNotifier = targetChangeNotifier;
    }

    /// <summary>
    /// Связывание объекта и нотификации свойства с исходным объектом.
    /// </summary>
    /// <param name="targetProperty">Свойство целевого объекта.</param>
    /// <returns>Связыватель.</returns>
    public ILinkedPropertyChanged Notify(Expression<Func<object>> targetProperty)
    {
        _sourceChangeNotifier.RegisterBeforeLinkedPropertyListener(_sourceProperty, _targetChangeNotifier,
                                                                    (string) targetProperty.GetName());
        return this;
    }

    /// <summary>
    /// Связывание объекта и действия с исходным объектом.
    /// </summary>
    /// <param name="action">Действие.</param>
    /// <returns>Связыватель.</returns>
    public ILinkedPropertyChanged Execute(Action action)
    {
        _sourceChangeNotifier.RegisterBeforeLinkedPropertyListener(_sourceProperty, _targetChangeNotifier,
                                                                    action);
        return this;
    }
}

Для связывания используются методы RegisterBeforeLinkedPropertyListener/RegisterAfterLinkedPropertyListener класса ChangeNotifier:
public abstract class ChangeNotifier : INotifyPropertyChanged
{
    ...
    private void RegisterBeforeLinkedPropertyListener(string linkedPropertyName,
                                                        ChangeNotifier targetObject,
                                                        string targetPropertyName)
    {
        RegisterLinkedPropertyListener(linkedPropertyName, targetObject, targetPropertyName,
                                        BeforeChangeLinkedChangeNotifierProperties);
    }

    private void RegisterBeforeLinkedPropertyListener(string linkedPropertyName,
                                                        ChangeNotifier targetObject,
                                                        Action action)
    {
        RegisterLinkedPropertyListener(linkedPropertyName, targetObject, action,
                                        BeforeChangeLinkedChangeNotifierProperties);
    }

    private static void RegisterLinkedPropertyListener(string linkedPropertyName,
                                            ChangeNotifier targetObject,
                                            string targetPropertyName,
                                            Dictionary<string, LinkedPropertyChangeNotifierListeners> linkedProperties)
    {
        GetOrCreatePropertyListeners(linkedPropertyName, linkedProperties).Register(targetObject, targetPropertyName);
    }

    private static void RegisterLinkedPropertyListener(string linkedPropertyName,
                                             ChangeNotifier targetObject,
                                             Action action,
                                             Dictionary<string, LinkedPropertyChangeNotifierListeners> linkedProperties)
    {
        GetOrCreatePropertyListeners(linkedPropertyName, linkedProperties).Register(targetObject, action);
    }

    private static LinkedPropertyChangeNotifierListeners GetOrCreatePropertyListeners(string linkedPropertyName,
                                                    Dictionary<string, LinkedPropertyChangeNotifierListeners> linkedProperties)
    {
        LinkedPropertyChangeNotifierListeners changeNotifierListeners;
        if (!linkedProperties.TryGetValue(linkedPropertyName, out changeNotifierListeners))
        {
            changeNotifierListeners = new LinkedPropertyChangeNotifierListeners();
            linkedProperties.Add(linkedPropertyName, changeNotifierListeners);
        }
        return changeNotifierListeners;
    }
    ...
}

Методы AfterNotify/BeforeNotify создают новые экземпляры связывателей для предоставления простого интерфейса связывания:
protected ILinkedPropertyChanged AfterNotify(Expression<Func<object>> property)
{
    var propertyCall = PropertyCallHelper.GetPropertyCall(property);
    return new AfterLinkedPropertyChanged((INotifyPropertyChanged) propertyCall.TargetObject,
                                            propertyCall.TargetPropertyName,
                                            this);
}

protected ILinkedPropertyChanged BeforeNotify(Expression<Func<object>> property)
{
    var propertyCall = PropertyCallHelper.GetPropertyCall(property);
    return new BeforeLinkedPropertyChanged((ChangeNotifier) propertyCall.TargetObject,
                                            propertyCall.TargetPropertyName,
                                            this);
}

protected ILinkedPropertyChanged AfterNotify<T>(T changeNotifier, Expression<Func<T, object>> property)
    where T : INotifyPropertyChanged
{
    return new AfterLinkedPropertyChanged(changeNotifier, property.GetName(), this);
}

protected ILinkedPropertyChanged BeforeNotify<T>(T changeNotifier, Expression<Func<T, object>> property)
    where T : ChangeNotifier
{
    return new BeforeLinkedPropertyChanged(changeNotifier, property.GetName(), this);
}

Из последнего листинга можно видеть, что связываемым объектом всегда выступает текущий объект, а в качестве исходного объекта может использоваться либо явно указанный экземпляр, либо полученный на основе разбора дерева выражения с помощью вспомогательного класса PropertyCallHelper. Зачастую исходный и связываемый объект совпадают.

 Еще раз на пальцах.

Объект ChangeNotifier содержит несколько коллекций, в которых хранятся данные о связанных с нотификацией свойства объектах, нотифицируемых свойствах этих объектов, а так же о действиях, которые должны быть вызваны до или после нотификации. Для предоставления простого интерфейса связывания объектов методы AfterNotify/BeforeNotify возвращают наследников ILinkedPropertyChanged, которые позволяют легко добавлять нужную информацию в коллекции. Методы ILinkedPropertyChanged возвращают исходный объект ILinkedPropertyChanged, что позволяет использовать цепочку вызовов для регистрации.
При нотификации об изменении свойства объект обращается к коллекциям связанных объектов и вызывает все необходимые зарегистрированные заранее действия.
ChangeNotifier предоставляет удобный интерфейс для изменения свойств объектов и нотификации об изменениях свойствах, который минимизирует затраты на разбор деревьев выражений.

Еще более простое объяснение.

Нет ничего проще, чем использовать ChangeNotifier в качестве базового класса для объектов, требующих реализации INotifyPropertyChanged. Мы применяем его класс для построения модели отображения и создания ViewModel. Это удобный инструмент, который экономит десятки часов разаботки на каждом нашем проекте. Короткое объяснение работы ChangeNotifier - он просто работает. И работает хорошо.



*Стоит упомянуть, что реализация извещения об изменении свойств при помощи деревьев выражений есть и в других фреймворках, например у класса NotificationObject в Microsoft prism 4, однако там разбор дерева ограничивается простейшими и базовыми случаями, что позволяет использовать нотификацию об изменении свойств только в самых типичных сценариях.

пятница, 6 сентября 2013 г.

Социальный РИКРОП

Сегодня отличный день для отличных новостей. Мы появились в LinkedIn. Следите за обновлениями, будет интересно!

четверг, 5 сентября 2013 г.

Фреймворк: Rikrop.Core.Wcf.Unity.dll

К оглавлению

Rikrop.Core.Wcf.Unity.dll

Библиотека содержит методы для клиентской и серверной регистрации WCF инфраструктуры приложения внутри контейнера. Регистраторы объединены в два пространства имен:
  • Rikrop.Core.Wcf.Unity.ClientRegistration 
  • Rikrop.Core.Wcf.Unity.ServerRegistration 
Структура объектов в каждом из них обеспечивает простую и удобную пошаговую регистрацию-настройку необходимых компонентов.

пятница, 23 августа 2013 г.

Rikrop.TimeTracker

Статистика проекта:

Строк кода: 20.000 C# + 7.000 xaml
Продолжительность разработки: 10 недель.
Технологии: .net 4.5, async/await, wpf, mvvm, Rikrop.Core, entity framework 5.0 code first, active directory, microsoft sql server 2012, wcf, nunit, Telerik .NET UI Controls, JustMock.

Для одной российской IT-компании мы сделали приложение для учёта рабочего времени сотрудников - TimeTracker. Основными целями приложения были: контроль отработанного времени сотрудников, управление рабочим графиком, ведение карточек сотрудников.


Список сотрудников компании и структурных подразделений извлекается из AD. На главной форме после авторизации можно увидеть текущий статус работников компании - кто находится в отпуске, кто сейчас на рабочем месте, сколько составляет недоработка или переработка за сегоднешний день и текущий отчётный период. Пользователи могут отмечать время прихода и ухода, программа сообщает о необходимости ввести причину опоздания и оперативно пересчитывает данные на основе действий сотрудника.


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


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


Нам очень понравилось тесное сотрудничество с заказчиком, регулярные демонстрации продукта пользователям и получение свежих отзывов о разрабатываемом функционале. Проект для своих небольших размеров имел много интересных и запомнившихся нам аспектов: интеграция с существующим доменом предприятия, вёрстка сложных форм по макетам заказчика, постоянная работа над повышением отзывчивости и удобства взаимодействия пользователя с приложением.

четверг, 22 августа 2013 г.

Rikrop.CadastreMap

Статистика проекта:


Строк кода: 7.000 C# + 1.000 xaml
Продолжительность разработки: 6 недель.
Технологии: .net 4.5, async/await, wpf, mvvm, Rikrop.Core, MigraDoc, MSTest, Moq.

Мы уже имеем опыт построения картографических систем на wpf и потому без страха взялись за проект отображения и редактирования карты земельных участков.
На вход приложение получает xaml-файл с контурами карты для отрисовки, сконвертированный из векторного отображения карты. Данные об участках сохраняются локально в xml-файл и отображаются при следующем входе в программу.

Основными сценариями при работе с приложением являются: создание и редактирование списка владельцев и групп участков, заполнение и просмотр данных карточек земельных участков, печать pdf-отчёта о земельном участке, просмотр и фильтрация карты для принятия решений о рациональности приобретения земельных участков.



АС Труд-Эксперт.NET

Статистика проекта:

Строк кода: 800.000 C# + 40.000 xaml
Продолжительность разработки: 150 недель.
Сайт проекта: www.trud-expert.net
Технологии: .net 4.0, wpf, mvvm, Prism, wcf, wf, WiX, entity framework 4.0, microsoft sql server 2008 R2, DevExpress reports, t4-templates.

Мы с удовольствием разрабатываем приложение для аттестации рабочих мест по условиям труда в Клинском институте охраны и условий труда. Приложение успешно внедрено внутри компании и распространяется среди внешних заказчиков. Постоянное развитие флагманского продукта компании даёт весомое конкурентное преимущество и повышает престиж компании на рынке.
Для нас является большой гордостью участие в разработке столь объёмного проекта. Опыт рефакторинга и развития крупных приложений оказался незаменимым в нашей повседневной работе.

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

Соблюдение и оптимизация agile-практик для решения текущих задач позволяют не снижать темпов развития на каждом этапе жизненного цикла. Мы пробовали scrum, kanban, распределенный scrum, но остановились на собственной модификации этих практик.
Работа над АС Труд-Эксперт.NET является очень важной для нас, и мы благодарны ЗАО "КИОУТ" за полученный опыт.

Фреймворк: Rikrop.Core.Framework.Unity.dll

К оглавлению

Rikrop.Core.Framework.Unity.dll

Библиотека содержит методы для помощи в регистрации объектов внутри контейнера. Несмотря на небольшой набор открытых методов, Rikrop.Core.Framework.Unity.dll составляет центральную часть любого нашего приложения. Рассмотрим всё по порядку.

namespace Rikrop.Core.Framework.Unity

Это пространство имен содержит единственный метод-расширение для регистрации провайдеров даты-времени в приложении
public static IUnityContainer RegisterDateTimeProviders(this IUnityContainer container) { ... }

namespace Rikrop.Core.Framework.Unity.Factories

Здесь расположена интереснейшая реализация регистрации фабрик создания объектов и регистрации этих фабрик в контейнере приложения.
Мы стараемся уменьшить количество кода при разработке, поэтому уделили особое внимание этой теме. Публичный интерфейс на первый взгляд ни о чём не говорит:
public static class FactoryExtension
{
    public static void RegisterFactory<TFactory>(this IUnityContainer container) { ... }
    public static void RegisterFactory(this IUnityContainer container, Type factoryInterfaceType) { ... }
    public static void RegisterAutoFactories(this IUnityContainer container, Assembly assembly) { ... }
}

Гораздо интереснее посмотреть на применение этих методов. Такую запись можно встретить во многих наших классах:
public class IndividualCalendarViewModel : ApplyWorkspace<IndividualCalendarView>
{
    ...
     public IndividualCalendarViewModel(long employeeId,
         IndividualStatisticsViewModel.ICtor individualStatisticsViewModel,
         IndividualScheduleViewModel.ICtor individualSheduleViewModelCtor)
    {
        _individualStatisticsViewModel = individualStatisticsViewModel.Create(employeeId);
        _individualSheduleViewModel = individualSheduleViewModelCtor.Create();
        ...
    }

    ...

    public interface ICtor
    {
        IndividualCalendarViewModel Create(long employeeId);
    }
}
А следующим образом можно зарегистрировать фабрики в контейнере:
foreach (var type in assemblies.SelectMany(assembly => assembly.DefinedTypes.Where(type => type.Name == "ICtor")))
{
    if (!container.IsRegistered(type))
    {
        container.RegisterFactory(type);
    }
}
Или в случае с использованием авотматических фабрик:
foreach (var assembly in assemblies)
{
    container.RegisterAutoFactories(assembly);
}

Как это работает?

Нужно заметить, что для создания фабрики объекта не нужно писать какой-либо дополнительный код. При этом объект нужного типа создаётся при помощи вызова метода  Create:
_individualSheduleViewModel = individualSheduleViewModelCtor.Create();
На самом деле при регистрации фабрики в контейнере мы проделываем всю необходимую работу за программиста - в памяти создаётся динамическая сборка, которая регистрируется в домене приложения. При помощи пространства имен System.Reflection.Emit мы генерируем IL-код с реальной фабрикой, создаем конструктор фабрики, принимающий на вход контейнер приложения и методы создания реального объекта с разрешением всех необходимых зависимостей.
Всё это звучит запутано и абстрактно, но мы хотим проиллюстрировать ту простоту, с которой решается одна из сложных проблем внутри генератора фабрик. В примере выше можно увидеть, что набор параметров конструктора отличается от набора параметров в методе Create для фабрики:
public class IndividualCalendarViewModel : ApplyWorkspace<IndividualCalendarView>
{
     public IndividualCalendarViewModel(long employeeId,
         IndividualStatisticsViewModel.ICtor individualStatisticsViewModel,
         IndividualScheduleViewModel.ICtor individualSheduleViewModelCtor)
    {
        ...
    }

    public interface ICtor
    {
        IndividualCalendarViewModel Create(long employeeId);
    }
}
Фабрика позволяет передавать часть параметров в конструктор типа явно, остальные параметры будут разрешаться из контейнера приложения.
private static void GenerateCreateMethods(TypeInfo typeInfo, MethodInfo resolveMethodInfo)
{
    // Для каждого метода в фабрике
    foreach (var factoryMethodInfo in typeInfo.FactoryInterfaceType.GetMethods())
    {
        // Извлечение возвращаемого параметра - целевого типа, объект которого будет создан
        Type targetType = GetTargetType(factoryMethodInfo);

        // Извлечение конструктора целевого типа
        ConstructorInfo targetTypeConstructor = FindTargetTypeConstructor(targetType);

        // Извлечение параметров фабричного метода
        ParameterInfo[] methodParametersInfo = factoryMethodInfo.GetParameters();

        // Построитель фактического метода создания объекта
        MethodBuilder methodBuilder = typeInfo.TypeBuilder.DefineMethod(factoryMethodInfo.Name, MethodAttributes.Public | MethodAttributes.Virtual, CallingConventions.HasThis,
                                                                factoryMethodInfo.ReturnType, methodParametersInfo.Select(p => p.ParameterType).ToArray());

        // Генератор IL-кода для метода
        ILGenerator il = methodBuilder.GetILGenerator();

        // Маппер параметров фабричного метода и конструктора целвого типа
        Mapper mapper = new Mapper(methodParametersInfo);

        int factoryMethodParameterIndex = 0;

        // Для каждого параметра конструктора целевого типа
        foreach (ParameterInfo targetTypeConstructorPrmInfo in targetTypeConstructor.GetParameters())
        {
            // Поиск соответствия параметру фабричного метода
            var factoryMethodParameterInfo = mapper.FindMethodParameterForConstructorParameter(targetTypeConstructorPrmInfo);
            if (factoryMethodParameterInfo == null)
            {
                // Если соответствие не найдено, генерируем код разрешения параметра из контейнера
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldstr, targetTypeConstructorPrmInfo.Name);
                il.Emit(OpCodes.Call, resolveMethodInfo.MakeGenericMethod(targetTypeConstructorPrmInfo.ParameterType));
            }
            else
            {
                // Передаём параметр фабричного метода в конструктор
                EmitLdarg(il, factoryMethodParameterIndex);
                factoryMethodParameterIndex++;
            }
        }

        il.Emit(OpCodes.Newobj, targetTypeConstructor);
        il.Emit(OpCodes.Ret);
    }
}

В результате, если конструктор объекта выглядит следующим образом:
public SomeClass(ILogger logger, int intPrm) { ... }
, а фабричный метод следующим:
public SomeClass Create(int intPrm);
, то генератор фабрики сформирует такую конструкцию:
public SomeClass Create(int intPrm)
{
    return new SomeClass(
        ResolveParameterWithException<ILogger>("logger"),
        intPrm);
}

 Зачем это использовать

К счастью, красота реализации фабрик в нашем фреймворке скрыта модификаторами доступа классов внутри библиотеки. Использовать же мощь фабрик проще простого. Пример регистрации фабрик в контейнере был приведен выше.
Конечно, альтернативой к автогенерации фабрик служит создание интерфейса для каждой сущности, регистрируемой в контейнере. Можно обойтись и вовсе без интерфейсов, но тогда внедрение зависимости теряет всякий смысл.
Нам кажется удобным использование автогенерируемых фабрик по нескольким причинам.
Во-первых, это быстро - достаточно написать несколько строк и фабрика готова:
public interface ICtor
{
    PunchViewModel Create();
}
Во-вторых, это гораздо универсальнее, чем просто объект - часто объект не может быть создан, пока не определены некоторые параметры:
public class PunchViewModel : Workspace<PunchView>
{
    private readonly EditDayActivityWorkspace.ICtor _editDayActivityWorkspaceCtor;

    public PunchViewModel(EditDayActivityWorkspace.ICtor editDayActivityWorkspaceCtor)
    {
        _editDayActivityWorkspaceCtor = editDayActivityWorkspaceCtor;
            
        ...
    }

    private async void ChangeState(long initiatorId)
    {
        var workspace = _editDayActivityWorkspaceCtor.Create(initiatorId);

        ...
    }
}
В-третьих, это значительно уменьшает количество кода. Не нужно создавать интерфейс, добавлять его регистрацию в контейнер. А в итоге не иметь гибкости, которую даёт фабрика.
В-четвертых, с помощью авторегистрации фабрик код приложения можно полностью отделить от внедрения зависимости - больше никаких антипаттернов.
В-пятых, это тестируемо:
var loginInfo = Mock.Create<LoginInfo>();
Mock.Arrange(() => loginInfo.AutoLogin).Returns(true);
Mock.Arrange(() => loginInfo.CurrentUserName).Returns("AnyUserName");

var loginInfoCtor = Mock.Create<LoginInfo.ICtor>(Behavior.Strict);
Mock.Arrange(() => loginInfoCtor.Create()).Returns(loginInfo);
В-шестых, это универсально. Фабрика может содержать несколько методов создания объекта с различным набором параметров. Фабрика может содержать методы, создающие объекты разных типов, а не только типа, в котором она определена. Таким образом, можно реализовать фабрику фабрик и больше никогда не торговать молотками!

А можно еще проще?

Ну, конечно! Контракт по имени интерфейса удобен, но не всегда возможен и оправдан. Нам захотелось сделать свою жизнь проще - и мы сделали:
[AttributeUsage(AttributeTargets.Interface)]
public class AutoFactoryAttribute : Attribute
{
}
public static void RegisterAutoFactories(this IUnityContainer container, Assembly assembly)
{
    foreach (var interfaceType in assembly.GetTypes().Where(type => type.IsInterface))
    {
        if (interfaceType.GetCustomAttribute<AutoFactoryAttribute>() != null)
        {
            container.RegisterFactory(interfaceType);
        }
    }
}
public class WorkplacesViewModel
{
    [AutoFactory]
    public interface IWorkplacesViewModel
    {
        WorkplacesViewModel Create();
    }
}

Способ создания и регистрации фабрик, реализованный в библиотеке  Rikrop.Core.Framework.Unity.dll очень помогает нам ускорить разработку и очистить код от мусорных интерфейсов, которые в DI используются зачастую исключительно для реализации регистрации в контейнере. Вместе с тем нужно помнить, что интерфейс служит прежде всего для определения контракта, и эту функцию фабрики тоже должны поддерживать.
public interface IWorkplacesViewModel
{
    IEnumerable<Workplace> Workplaces { get; }
}

public class ReadOnlyWorkplacesViewModel : IWorkplacesViewModel
{
    ...

    [AutoFactory]
    public interface IWorkplacesViewModel
    {
        [Creates(typeof(ReadOnlyWorkplacesViewModel))]
        IWorkplacesViewModel Create();
    }
}

public class EditableWorkplacesViewModel : IWorkplacesViewModel
{
    ...

    [AutoFactory]
    public interface IWorkplacesViewModel
    {
        [Creates(typeof(EditableWorkplacesViewModel))]
        IWorkplacesViewModel Create();
    }
}

А можно еще проще?

Наверняка, да. Мы выбрали баланс между гибкостью и простотой. Созданный инструмент позволил нам сосредоточиться при разработке на задачах развития функционала и поддержания целостной архитектуры приложения, не отвлекаясь на написание избыточного кода, необходимо для явной реализации dependency injection.

вторник, 20 августа 2013 г.

Фреймворк: Rikrop.Core.Framework.dll

К оглавлению

Rikrop.Core.Framework.dll

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

 namespace Rikrop.Core.Framework

Сюда мы поместили те классы и инструменты, которые являются инфраструктурными.

public static class CollectionExtentions

Поскольку коллекции в .NET развивались вместе с самим фреймворком, не каждый из контейнеров получил удобный интерфейс манипуляции данными. Здесь мы собрали методы расширения, которые решают эту проблему. Мы вставляем, добавляем, перебираем, ищем и осуществляем слияние.

public static class CryptoHelper

К счастью, в .NET есть классы для работы алгоритмами шифрования, цифровой подписи и хеширования. К сожалению, их интерфейс довольно громоздкий. Для себя мы сделали маленький класс с единственным методом:

public static string GetShaHash(string str) { ... }

public class DateRange

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

public interface ITodayNowProvider + public class TodayNowProvider + public interface IDateTimeProvider + public class DateTimeProvider : IDateTimeProvider

Мы често работаем с временем и столкнулись с проблемой покрытия кода, связанного с определением времени относительно текущего. А еще очень часто оказалось важным иметь доступ к таким данным как "текущий месяц", "прошлая неделя". Реализация  DateTimeProvider и чудесный класс DateRange дали возможность с лёгкостью манипулировать типов данных DateTime. Вот простой пример mock для тестирования функционала, использующего провайдер даты-времени.
var today = new DateTime(2013, 05, 2);
var dateTimeProvider = Mock.Create<IDateTimeProvider>();
Mock.Arrange(() => dateTimeProvider.Today).Returns(today);

А вот вкратце и без излишних комментариев и интерфейс IDateTimeProvider
public interface IDateTimeProvider
{
    DateTime Now { get; }
    DateTime UtcNow { get; }
    DateTime Today { get; }

    DateRange GetLogicalDateRange(DateTime dateFrom, DateTime dateTo, int dayStartHour);
    DateRange GetFactDateRange(DateTime logicDateFrom, DateTime logicDateTo, int dayStartHour);
    DateRange MaxRange();
    DateRange GetMonthAgo(int monthCount = -1);
    DateRange GetCurrentMonth();
    DateRange GetToday();
    DateRange GetYesterday();
    DateRange GetCurrentWeek();
    DateRange GetLastWeek();
    DateRange GetTwoWeeksAgo();
    DateRange GetLastMonth();
}

public static class ExpressionHelper

Мы активно работаем над строгой типизацией и статической проверкой в коде, поэтому активно используем работу с деревьями выражений (Expressions). В этом классе собраны важные, хоть и редкоприменимые методы для разбора и получения информации из деревьев выражений.

public static class SerializerExtention

В .NET нет такого удобного инструмента как generic-serializer. У нас он есть.

public interface ITimer + public class SimpleTimer : ITimer + public class RepeatableExecutor

Мы решили абстрагироваться от множества реализаций таймеров, которые есть в .NET и переносимо между платформами решить наиболее частую задачу, в которой применяются таймера - повторяемый запуск определенного события.

namespace Rikrop.Core.Framework.ActiveDirectory

Интеграция приложений с AD является важной темой. В .NET на данный момент существует несколько подходов к работе с сущностями домена, однако для типовых задач часто хватает ограниченного набора функций. Основным классом здесь является ADManager.
public interface IADManager
{
    IEnumerable<ADUserEntity> GetUsersInGroup(string groupName, bool recursive);
    IEnumerable<ADUserEntity> GetAllUsers();
    IEnumerable<ADUserEntity> GetUsersWithNameAndLastName();
    IEnumerable<ADGroupEntity> GetUserGroups(string userIdentity);
    IEnumerable<ADGroupEntity> GetAllGroups();
    bool ValidateAuthorization();
}
Стоит отметить, что это лишь базовый набор функций, каждое приложение ставит свои требования и единого интерфейса для интеграции с различной структурой AD заказчиков достичь очень проблематично, но мы всегда строим свои системы максимально настраиваемыми и универсальными.

namespace Rikrop.Core.Framework.Exceptions

Для обработки исключений мы используем механизм, схожий с Exception Handling Application Block из Enterprise library. Вместе с этим, мы постарались упростить механизм обработки исключений.
public interface IExceptionHandler
{
    bool Handle(Exception exception);
}
У нас нет готовых обработчиков, пока двух одинаковых решений по обработке исключений в наших проектах не встретилось. Но фреймворк поддерживается в максимально актуальном состоянии и как только типовое решение появится - мы сразу добавим его. В Enterprise library логирование может быть организовано через подсистему обработки ошибок, но у нас мухи отделены от перехвата исключений.

namespace Rikrop.Core.Framework.Logging

Поскольку логирование событий необходимо практически в каждом приложении, мы вынесли инфраструктуру для логирования в общую сборку.
public interface ILoggerFactory
{
    ILogger CreateForSource(string logSource);
}

public interface ILogger
{
    void Log<TRecord>(TRecord record) where TRecord : ILogRecord;
}

public interface ILogRecord
{
    LogRecordLevel LogLevel { get; }
    string Message { get; }
    LogRecordDataValue[] DataValues { get; }
    bool HasDataValues { get; }
}
Мы не стали реализовывать в базовой библиотеке конкретные логгеры, вместо этого можно подключить библиотеки Rikrop.Core.Logging.dll и Rikrop.Core.Logging.NLog.dll. По необходимости всегда можно написать свой логгер, но пока необходимости в этом не возникало.

namespace Rikrop.Core.Framework.Services

Взаимодействие с уровнем сервисов занимает очень важное место в нашем фреймворке. Если рассматривать клиентскую сторону взаимодействия, то мы приняли концепцию работы с сервисами через Executor'ы, которые могут обладать некоторыми дополнительными свойствами в зависимости от контекста. Например, для работы в ViewModel на время запуска метода сервиса может исполняться триггер для извещения интерфейса о том, что приложение занято. Точкой входа является интерфейс
public interface IServiceExecutor<out TService>
{
    Task Execute(Func<TService, Task> action);
    Task<TResult> Execute<TResult>(Func<TService, Task<TResult>> func);
}

Фреймворк: Rikrop.Core обзор

Эта статья служит оглавлением и кратким справочником по фреймворку для быстрой разработки приложений Rikrop.Core.
Прежде всего нужно сказать, что мы приняли для наших проектов правило использовать Unity в качестве инструмента DI. Мы используем паттерн Model-View-ViewModel и стараемся сделать код максимально тестируемым.

Rikrop.Core.Framework.dll

Эта библиотека содержит множество наиболее обобщенных инструментов, которые находят применение из проекта в проект. Здесь находятся методы-расширения для стандартных контейнеров .NET, удобные реализации таймера, дополнительные типы данных для манипуляций с датами, классы-обёртки для работы с Active directory, инструменты разбора деревьев логических выражений, инфраструктура для работы с исключениями, абстракции для логирования и работы с сервисным слоем приложения.

Rikrop.Core.Framework.Unity.dll

Библиотека с методами-расширениями IUnityContainer для регистрации и автоматической генерации кода фабрик объектов, предназначенных для избавления от избыточного кода для реализации dependency injection и ускорения процесса разработки приложений.

Rikrop.Core.Wpf.dll

Фреймворк: Rikrop.Core.Wpf.dll. Часть 0, INotifyPropertyChanged
Нам было жизненно необходимо сделать разработку на wpf максимально удобной для нас, при этом тесно интегрировать презентационный слой приложения с нашим фреймворком. Как и во всех проектах фреймворка, мы ставили задачу гибкости и прозрачности использования разрабатываемого инструмента. WPF-библиотека является надстройкой и расширением над переносимой сборкой Rikrop.Core.Presentation.Portable.dll. Основные блоки библиотеки:
  • Набор настраиваемых элементов управления на все случае жизни.
  • Элементы управления и хелперы для максимального контроля за кастомизацией отображения окна приложения.
  • Расширение механизма команд и реализации удобных средств создания и управления состоянием команд.
  • Инфраструктура для удобной работы с коллекциями - коллекции, нотифицирующие об изменениях, постраничные загрузчики коллекций, динамически загружаемые коллекции.
  • Наиболее полный и пополняемый набор из 50+ конвертеров значений. 
  • Пополняемый набор частоупотребимых и незаменимых во многих проектах attached properties.
  • Обработчики исключений уровня приложения, отображающие информацию об исключении в понятном виде. 
  • Механизмы для управления визуализацией отображений в приложении.
  • Базовые реализации объектов, нотифицирующих об изменениях своих свойств.
  • Полная и самодостаточная реализация классов для быстрой разработки в рамках паттерна mvvm.
  • Набор инфраструктурных классов для обеспечения отзывчивости приложения.
Библиотека Rikrop.Core.Wpf.dll является одной из самых объёмных в фреймворке и занимает центральное положение при разработке удобного, красивого и отзывчивого отображения в наших проектах.

Rikrop.Core.Wcf.Unity.dll

В этой библиотеке мы собрали методы для простой и быстрой регистрации WCF-сервисов без использования громоздких конструкций в конфигурационных файлах. Это одна из двух библиотек, которая сделала использование wcf в наших приложениях совершенно прозрачным. Созданная инфраструктура позволила нам не заботиться об актуальности конфигурации сервисов и вызывать wcf так же просто, как и локальные методы.
Rikrop.Core.Dataflow.dll

Rikrop.Core.Exceptions.dll

Rikrop.Core.Framework.Portable.dll

Rikrop.Core.Presentation.Portable.dll

Rikrop.Core.Wcf.Portable.dll

Rikrop.Core.Logging.dll

Rikrop.Core.Logging.NLog.dll

Rikrop.Core.MapReduce.dll

Rikrop.Core.Network.dll

Rikrop.Core.Testing.dll

Rikrop.Core.Threading.dll

Rikrop.Core.Wcf.dll


Rikrop.Core.Wpf.Unity.dll