disclaimer: данная статья является пошаговым руководством, которое поможет ознакомиться с основными возможностями Incoding Framework. Результатом следования данному руководству будет покрытое юнит-тестами приложение, реализующее работу с БД (CRUD + data filters). О Incoding framework ранее уже были статьи на habrahabr, но в них раскрываются отдельные части инструмента.
Часть 0. Введение
Для начала приведем краткое описание фреймворка. Incoding Framework состоит из трех пакетов: Incoding framework – back-end проекта, Incoding Meta Language – front-end проекта и Incoding tests helpers – юнит-тесты для back-end’а. Эти пакеты устанавливаются независимо друг от друга, что позволяет интегрировать фреймворк в проект частями: Вы можете подключить только клиентскую или только серверную часть (тесты очень сильно связаны с серверной частью, поэтому их можно позиционировать как дополнение).
В проектах, написанных на Incoding Framework, в качестве серверной архитектуры используется CQRS. В качестве основного инструмента построения клиентской части используется Incoding Meta Language. В целом Incoding Framework покрывает весь цикл разработки приложения.
Типичный solution, созданный с помощью Incoding Framework, имеет 3 проекта:
- Domain (class library) — отвечает за бизнес-логику и работу с базой данных.
- UI (ASP.NET MVC project) - клиентская часть, основанная на ASP.NET MVC.
- UnitTests (class library) — юнит-тесты для Domain.
Domain
После установки пакета Incoding framework через Nuget в проект помимо необходимых dll устанавливается файл Bootstrapper.cs. Основная задача этого файла — инициализация приложения: инициализация логирования, регистрация IoC, установка настроек Ajax-запросов и пр. В качестве IoC framework по умолчанию устанавливается StructureMap, однако есть провайдер для Ninject, а также есть возможность написания своих реализаций.
namespace Example.Domain
{
#region << Using >>
using System;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Web.Mvc;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using FluentValidation;
using FluentValidation.Mvc;
using Incoding.Block.IoC;
using Incoding.Block.Logging;
using Incoding.CQRS;
using Incoding.Data;
using Incoding.EventBroker;
using Incoding.Extensions;
using Incoding.MvcContrib;
using NHibernate.Tool.hbm2ddl;
using StructureMap.Graph;
#endregion
public static class Bootstrapper
{
public static void Start()
{
//Initialize LoggingFactory
LoggingFactory.Instance.Initialize(logging =>
{
string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log");
logging.WithPolicy(policy => policy.For(LogType.Debug).Use(FileLogger.WithAtOnceReplace(path,
() => "Debug_{0}.txt".F(DateTime.Now.ToString("yyyyMMdd")))));
});
//Initialize IoCFactory
IoCFactory.Instance.Initialize(init =>
init.WithProvider(new StructureMapIoCProvider(registry =>
{
registry.For<IDispatcher>().Use<DefaultDispatcher>();
registry.For<IEventBroker>().Use<DefaultEventBroker>();
registry.For<ITemplateFactory>().Singleton().Use<TemplateHandlebarsFactory>();
//Configure FluentlyNhibernate
var configure = Fluently
.Configure()
.Database(MsSqlConfiguration.MsSql2008
.ConnectionString(ConfigurationManager.ConnectionStrings["Example"].ConnectionString))
.Mappings(configuration => configuration.FluentMappings
.AddFromAssembly(typeof(Bootstrapper).Assembly))
.ExposeConfiguration(cfg => new SchemaUpdate(cfg).Execute(false, true))
.CurrentSessionContext<NhibernateSessionContext>();
registry.For<INhibernateSessionFactory>()
.Singleton()
.Use(() => new NhibernateSessionFactory(configure));
registry.For<IUnitOfWorkFactory>().Use<NhibernateUnitOfWorkFactory>();
registry.For<IRepository>().Use<NhibernateRepository>();
//Scan currenlty Assembly and registrations all Validators and Event Subscribers
registry.Scan(r =>
{
r.TheCallingAssembly();
r.WithDefaultConventions();
r.ConnectImplementationsToTypesClosing(typeof(AbstractValidator<>));
r.ConnectImplementationsToTypesClosing(typeof(IEventSubscriber<>));
r.AddAllTypesOf<ISetUp>();
});
})));
ModelValidatorProviders.Providers
.Add(new FluentValidationModelValidatorProvider(new IncValidatorFactory()));
FluentValidationModelValidatorProvider.Configure();
//Execute all SetUp
foreach (var setUp in IoCFactory.Instance.ResolveAll<ISetUp>().OrderBy(r => r.GetOrder()))
{
setUp.Execute();
}
var ajaxDef = JqueryAjaxOptions.Default;
ajaxDef.Cache = false; //Disable Ajax cache
}
}
}
Далее в Domain дописываются команды (Command) и запросы (Query), которые выполняют операции с базой данных либо какие-то другие действия, связанные с бизнес-логикой приложения.
UI
Пакет Incoding Meta Language при установке добавляет в проект необходимые dll, а также файлы IncodingStart.cs и DispatcherController.cs (часть MVD) необходимые для работы с Domain.
public static class IncodingStart
{
public static void source Start()
{
Bootstrapper.Start();
new DispatcherController(); // init routes
}
}
public class DispatcherController : DispatcherControllerBase
{
#region Constructors
public DispatcherController()
: base(typeof(Bootstrapper).Assembly) { }
#endregion
}
После установки в UI дописывается клиентская логика с использованием IML.
UnitTests
При установке Incoding tests helpers в проект добавляется файл MSpecAssemblyContext.cs, в котором настраивается connection к тестовой базе данных.
public class MSpecAssemblyContext : IAssemblyContext
{
#region IAssemblyContext Members
public void OnAssemblyStart()
{
//Настройка подключения к тестовой БД
var configure = Fluently
.Configure()
.Database(MsSqlConfiguration.MsSql2008
.ConnectionString(ConfigurationManager.ConnectionStrings["Example_Test"].ConnectionString)
.ShowSql())
.Mappings(configuration => configuration.FluentMappings.AddFromAssembly(typeof(Bootstrapper).Assembly));
PleasureForData.StartNhibernate(configure, true);
}
public void OnAssemblyComplete() { }
#endregion
}
Часть 1. Установка
Итак, приступим к выполнению поставленной в disclamer задаче - начнем писать наше приложение. Первый этап создания приложения — создание структуры solution'а проекта и добавление projects в него. Solution проекта будет называться Example и, как уже было сказано во введении, будет иметь три projects. Начнем с project'а, который будет отвечать за бизнес-логику приложения — с Domain.
Создаем class library Domain.
Далее перейдем к клиентской части — создаем и устанавливаем как запускаемый пустой проект ASP.NET Web Application UI с сылками на MVC packages.
И наконец, добавим class library UnitTests, отвечающую за юнит-тестирование.
Внимание: хотя юнит-тесты и не являются обязательной частью приложения, мы рекомендуем Вам всегда покрывать код тестами, так как это позволит в будущем избежать множества проблем с ошибками в коде за счет автоматизации тестирования.
После выполнения всех вышеперечисленных действий должен получится следующий Solution:
После создания структуры Solution'а необходимо собственно установить пакеты Incoding Framework из Nuget в соответствующие projects.
Установка происходит через Nuget. Для всех projects алгоритм установки один:
- Кликните правой кнопкой по проекту и выберите в контекстном меню пункт Manage Nuget Packages...
- В поиске введите incoding
- Выберите нужный пакет и установите его
Сначала устанавливаем Incoding framework в Domain.
Далее добавляем в файл Domain -> Infrastructure -> Bootstrapper.cs ссылку на StructureMap.Graph.
В UI нужно установить два пакета:
Внимание: убедитесь, что для References -> System.Web.Mvc.dll свойство «Copy Local» установлено в «true»
Теперь файл Example.UI -> Views -> Shared -> _Layout.cshtml измените таким образом, чтобы он выглядел так:
@using Incoding.MvcContrib
<!DOCTYPE html>
<html >
<head>
<script type="text/javascript" src="@Url.Content("~/Scripts/jquery-1.9.1.min.js")"> </script>
<script type="text/javascript" src="@Url.Content("~/Scripts/jquery-ui-1.10.2.min.js")"></script>
<script type="text/javascript" src="@Url.Content("~/Scripts/underscore.min.js")"> </script>
<script type="text/javascript" src="@Url.Content("~/Scripts/jquery.form.min.js")"> </script>
<script type="text/javascript" src="@Url.Content("~/Scripts/jquery.history.js")"> </script>
<script type="text/javascript" src="@Url.Content("~/Scripts/jquery.validate.min.js")"> </script>
<script type="text/javascript" src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")"> </script>
<script type="text/javascript" src="@Url.Content("~/Scripts/handlebars-1.1.2.js")"> </script>
<script type="text/javascript" src="@Url.Content("~/Scripts/incoding.framework.min.js")"> </script>
<script type="text/javascript" src="@Url.Content("~/Scripts/incoding.meta.language.contrib.js")"> </script>
<script type="text/javascript" src="@Url.Content("~/Scripts/bootstrap.min.js")"> </script>
<link rel="stylesheet" type="text/css" href="@Url.Content("~/Content/bootstrap.min.css")">
<link rel="stylesheet" type="text/css" href="@Url.Content("~/Content/themes/base/jquery.ui.core.css")">
<link rel="stylesheet" type="text/css" href="@Url.Content("~/Content/themes/base/jquery.ui.datepicker.css")">
<link rel="stylesheet" type="text/css" href="@Url.Content("~/Content/themes/base/jquery.ui.dialog.css")">
<link rel="stylesheet" type="text/css" href="@Url.Content("~/Content/themes/base/jquery.ui.theme.css")">
<link rel="stylesheet" type="text/css" href="@Url.Content("~/Content/themes/base/jquery.ui.menu.css")">
<script>
TemplateFactory.Version = '@Guid.NewGuid().ToString()';
</script>
</head>
@Html.Incoding().RenderDropDownTemplate()
<body>
@RenderBody()
</body>
</html>
Осталось добавить ссылку на Bootstrapper.cs в файлы Example.UI -> App_Start -> IncodingStart.cs и Example.UI -> Controllers -> DispatcherController.cs.
Внимание: если вы используете MVC5, то для работы framework'а необходимо добавить следующий код в файл Web.config
<dependentAssembly>
<assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" />
</dependentAssembly>
Осталось установить Incoding tests helpers в UnitTests и добавить ссылку на Bootstrapper.cs в Example.UnitTests -> MSpecAssemblyContext.cs.
Последний этап подготовки проектов к работе — создание структуры папок для projects.
Добавьте следующие папки в проект Example.Domain:
- Operations — command и query проекта
- Persistences — сущности для маппинга БД
- Specifications — where и order спецификации для фильтрации данных при запросах
В проекте Example.UnitTests создайте такую же структуру папок как и в Example.Domain.
Часть 2. Настройка DB connection
Для начала создадим БД, с которыми будем работать. Откройте SQL Managment Studio и создайте две базы данных: Example и Example_test.
Для того чтобы работать с БД в необходимо настроить connection. Добавьте в файлы Example.UI -> Web.config и Example.UnitTests -> app.config connection string к базе данных:
<connectionStrings>
<add name="Example" connectionString="Data Source=INCODING-PC\SQLEXsource SS;Database=Example;Integrated Security=false; User Id=sa;Password=1" providerName="System.Data.SqlClient" />
<add name="Example_Test" connectionString="Data Source=INCODING-PC\SQLEXsource SS;Database=Example_Test;Integrated Security=true" providerName="System.Data.SqlClient" />
</connectionStrings>
В файле Example.Domain -> Infrastructure -> Bootstrapper.cs зарегистрируйте по ключу «Example» соответствующую строку подключения:
//Configure FluentlyNhibernate
var configure = Fluently
.Configure()
.Database(MsSqlConfiguration.MsSql2008.ConnectionString(ConfigurationManager
.ConnectionStrings["Example"].ConnectionString))
.Mappings(configuration => configuration.FluentMappings
.AddFromAssembly(typeof(Bootstrapper).Assembly))
.ExposeConfiguration(cfg => new SchemaUpdate(cfg).Execute(false, true))
.CurrentSessionContext(); //Configure data base
В файле Example.UnitTests -> MSpecAssemblyContext.cs зарегистрируйте по ключу «Example_Test» строку подключения к базе данных для тестов:
//Configure connection to Test data base
var configure = Fluently
.Configure()
.Database(MsSqlConfiguration.MsSql2008
.ConnectionString(ConfigurationManager.ConnectionStrings["Example_Test"].ConnectionString)
.ShowSql())
.Mappings(configuration => configuration.FluentMappings
.AddFromAssembly(typeof(Bootstrapper).Assembly));
Внимание: базы данных Example и Example_Test должны существовать.
Часть 3. CRUD
После выполнения всех приведенных выше действий мы подошли к самой интересной части — написанию кода, реализующего CreateReadUpdateDelete-функционал приложения. Для начала необходимо создать класс сущности, которая будет маппиться на БД. В нашем случае это будет Human.cs, который добавим в папку Example.Domain -> Persistences.
Human.cs
namespace Example.Domain
{
#region << Using >>
using System;
using Incoding.Data;
#endregion
public class Human : IncEntityBase
{
#region Properties
public virtual DateTime Birthday { get; set; }
public virtual string FirstName { get; set; }
public virtual string Id { get; set; }
public virtual string LastName { get; set; }
public virtual Sex Sex { get; set; }
#endregion
#region Nested Classes
public class Map : NHibernateEntityMap<Human>
{
#region Constructors
protected Map()
{
IdGenerateByGuid(r => r.Id);
MapEscaping(r => r.FirstName);
MapEscaping(r => r.LastName);
MapEscaping(r => r.Birthday);
MapEscaping(r => r.Sex);
}
#endregion
}
#endregion
}
public enum Sex
{
Male = 1,
Female = 2
}
}
Наш класс содержит несколько полей, в которые мы будем записывать данные, и вложенный класс маппинга (class Map).
Заметка: после создания класса Human Вам больше не нужно производить никаких действий (дописывание xml-маппинга) благодаря FluentNhibernate.
Теперь добавим команды (Command) и запросы (Query), которые будут отвечать за реализацию CRUD-операций. Первая комманда будет отвечать за добавление новой или изменение существующей записи типа Human. Комманда довольно простая: мы либо получаем из Repository сущность по ключу (Id), либо, если такой сущности нет, создаем новую. В обоих случаях сущность получает значения, которые указаны в свойствах класса AddOrEditHumanCommand. Добавим файл Example.Domain -> Operations -> AddOrEditHumanCommand.cs в проект.
AddOrEditHumanCommand.cs
namespace Example.Domain
{
#region << Using >>
using System;
using FluentValidation;
using Incoding.CQRS;
using Incoding.Extensions;
#endregion
public class AddOrEditHumanCommand : CommandBase
{
#region Properties
public DateTime BirthDay { get; set; }
public string FirstName { get; set; }
public string Id { get; set; }
public string LastName { get; set; }
public Sex Sex { get; set; }
#endregion
public override void Execute()
{
var human = Repository.GetById<Human>(Id) ?? new Human();
human.FirstName = FirstName;
human.LastName = LastName;
human.Birthday = BirthDay;
human.Sex = Sex;
Repository.SaveOrUpdate(human);
}
}
}
Следующая часть CRUD — Read — запрос на чтение сущностей из базы. Добавьте файл Example.Domain -> Operations -> GetPeopleQuery.cs.
GetPeopleQuery.cs
namespace Example.Domain
{
#region << Using >>
using System.Collections.Generic;
using System.Linq;
using Incoding.CQRS;
#endregion
public class GetPeopleQuery : QueryBase<List<GetPeopleQuery.Response>>
{
#region Properties
public string Keyword { get; set; }
#endregion
#region Nested Classes
public class Response
{
#region Properties
public string Birthday { get; set; }
public string FirstName { get; set; }
public string Id { get; set; }
public string LastName { get; set; }
public string Sex { get; set; }
#endregion
}
#endregion
protected override List<Response> ExecuteResult()
{
return Repository.Query<Human>().Select(human => new Response
{
Id = human.Id,
Birthday = human.Birthday.ToShortDateString(),
FirstName = human.FirstName,
LastName = human.LastName,
Sex = human.Sex.ToString()
}).ToList();
}
}
}
И оставшаяся часть функционала — это Delete — удаление записей из БД по ключу (Id). Добавьте файл Example.Domain -> Operations -> DeleteHumanCommand.cs.
DeleteHumanCommand.cs
namespace Example.Domain
{
#region << Using >>
using Incoding.CQRS;
#endregion
public class DeleteHumanCommand : CommandBase
{
#region Properties
public string HumanId { get; set; }
#endregion
public override void Execute()
{
Repository.Delete<Human>(HumanId);
}
}
}
Для того чтобы наполнить БД начальными данными добавьте файл Example.Domain -> InitPeople.cs — этот файл наследуется от интерфейса ISetUp.
ISetup
using System;
namespace Incoding.CQRS
{
public interface ISetUp : IDisposable
{
int GetOrder();
void Execute();
}
}
Все экземпляры классов, унаследованных от ISetUp, регистрируются через IoC в Bootstrapper.cs (был приведен во введении). После регистрации они запускаются на исполнение (public void Execute()) по порядку (public int GetOrder()).
InitPeople.cs
namespace Example.Domain
{
#region << Using >>
using System;
using Incoding.Block.IoC;
using Incoding.CQRS;
using NHibernate.Util;
#endregion
public class InitPeople : ISetUp
{
public void Dispose() { }
public int GetOrder()
{
return 0;
}
public void Execute()
{
//получение Dispatcher для выполнения Query и Command
var dispatcher = IoCFactory.Instance.TryResolve<IDispatcher>();
//не добавлять записи, если в базе есть хотя бы одна запись
if (dispatcher.Query(new GetEntitiesQuery<Human>()).Any())
return;
//добавление записей
dispatcher.Push(new AddOrEditHumanCommand
{
FirstName = "Hellen",
LastName = "Jonson",
BirthDay = Convert.ToDateTime("06/05/1985"),
Sex = Sex.Female
});
dispatcher.Push(new AddOrEditHumanCommand
{
FirstName = "John",
LastName = "Carlson",
BirthDay = Convert.ToDateTime("06/07/1985"),
Sex = Sex.Male
});
}
}
}
Back-end реализация CRUD готова. Теперь надо добавить клиентский код. Также как и в случае с серверной частью, начнем реализацию с части создания/редактирования записи. Добавьте файл Example.UI -> Views -> Home -> AddOrEditHuman.cshtml. Представленный IML-код создает стандартную html-форму и работает с командой AddOrEditHumanCommand, отправляя на сервер соответствующий Ajax-запрос.
AddOrEditHuman.cshtml
@using Example.Domain
@using Incoding.MetaLanguageContrib
@using Incoding.MvcContrib
@model Example.Domain.AddOrEditHumanCommand
@*Формирование формы для Ajax-отправки на выполнение AddOrEditHumanCommand*@
@using (Html.When(JqueryBind.Submit)
@*Прерывание поведения по умолчанию и отправка формы через Ajax*@
.PreventDefault()
.Submit()
.OnSuccess(dsl =>
{
dsl.WithId("PeopleTable").Core().Trigger.Incoding();
dsl.WithId("dialog").JqueryUI().Dialog.Close();
})
.OnError(dsl => dsl.Self().Core().Form.Validation.Refresh())
.AsHtmlAttributes(new
{
action = Url.Dispatcher().Push(new AddOrEditHumanCommand()),
enctype = "multipart/form-data",
method = "POST"
})
.ToBeginTag(Html, HtmlTag.Form))
{
<div>
@Html.HiddenFor(r => r.Id)
@Html.ForGroup(r => r.FirstName).TextBox(control => control.Label.Name = "First name")
<br/>
@Html.ForGroup(r => r.LastName).TextBox(control => control.Label.Name = "Last name")
<br/>
@Html.ForGroup(r => r.BirthDay).TextBox(control => control.Label.Name = "Birthday")
<br/>
@Html.ForGroup(r => r.Sex).DropDown(control => control.Input.Data = typeof(Sex).ToSelectList())
</div>
<div>
<input type="submit" value="Save"/>
@*Закрытие диалога*@
@(Html.When(JqueryBind.Click)
.PreventDefault()
.StopPropagation()
.Direct()
.OnSuccess(dsl => { dsl.WithId("dialog").JqueryUI().Dialog.Close(); })
.AsHtmlAttributes()
.ToButton("Cancel"))
</div>
}
Далее следует template, который является шаблоном для загрузки данных, полученных от GetPeopleQuery. Здесь описывается таблица, которая будет отвечать не только за вывод данных, но и за удаление и редактирование отдельных записей: добавьте файл Example.UI -> Views -> Home -> HumanTmpl.cshtml.
HumanTmpl.cshtml
@using Example.Domain
@using Incoding.MetaLanguageContrib
@using Incoding.MvcContrib
@{
using (var template = Html.Incoding().Template<GetPeopleQuery.Response>())
{
<table class="table">
<thead>
<tr>
<th>
First name
</th>
<th>
Last name
</th>
<th>
Birthday
</th>
<th>
Sex
</th>
<th></th>
</tr>
</thead>
<tbody>
@using (var each = template.ForEach())
{
<tr>
<td>
@each.For(r => r.FirstName)
</td>
<td>
@each.For(r => r.LastName)
</td>
<td>
@each.For(r => r.Birthday)
</td>
<td>
@each.For(r => r.Sex)
</td>
<td>
@*Кнопка открытия диалога для редактирования*@
@(Html.When(JqueryBind.Click)
.AjaxGet(Url.Dispatcher().Model<AddOrEditHumanCommand>(new
{
Id = each.For(r => r.Id),
FirstName = each.For(r => r.FirstName),
LastName = each.For(r => r.LastName),
BirthDay = each.For(r => r.Birthday),
Sex = each.For(r => r.Sex)
})
.AsView("~/Views/Home/AddOrEditHuman.cshtml"))
.OnSuccess(dsl => dsl.WithId("dialog").Behaviors(inDsl =>
{
inDsl.Core().Insert.Html();
inDsl.JqueryUI().Dialog.Open(option =>
{
option.Resizable = false;
option.Title = "Edit human";
});
}))
.AsHtmlAttributes()
.ToButton("Edit"))
@*Кнопка удаления записи*@
@(Html.When(JqueryBind.Click)
.AjaxPost(Url.Dispatcher().Push(new DeleteHumanCommand() { HumanId = each.For(r => r.Id) }))
.OnSuccess(dsl => dsl.WithId("PeopleTable").Core().Trigger.Incoding())
.AsHtmlAttributes()
.ToButton("Delete"))
</td>
</tr>
}
</tbody>
</table>
}
}
Задача открытия диалогового окна достаточно распространена, поэтому код, отвечающий за это действие, можно вынести в extension.
Последняя часть — изменение стартовой страницы так, чтобы при ее загрузке выполнялся Ajax-запрос на сервер для получения данных от GetPeopleQuery и отображения их через HumanTmpl: измените файл Example.UI -> Views -> Home -> Index.cshtml так, чтобы он соответствовал представленному ниже коду.
Index.cshtml
@using Example.Domain
@using Incoding.MetaLanguageContrib
@using Incoding.MvcContrib
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div id="dialog"></div>
@*Загрузка записей, полученных из GetPeopleQuery, через HumanTmpl*@
@(Html.When(JqueryBind.InitIncoding)
.AjaxGet(Url.Dispatcher().Query(new GetPeopleQuery()).AsJson())
.OnSuccess(dsl => dsl.Self().Core().Insert.WithTemplateByUrl(Url.Dispatcher().AsView("~/Views/Home/HumanTmpl.cshtml")).Html())
.AsHtmlAttributes(new { id = "PeopleTable" })
.ToDiv())
@*Кнопка добавления новой записи*@
@(Html.When(JqueryBind.Click)
.AjaxGet(Url.Dispatcher().AsView("~/Views/Home/AddOrEditHuman.cshtml"))
.OnSuccess(dsl => dsl.WithId("dialog").Behaviors(inDsl =>
{
inDsl.Core().Insert.Html();
inDsl.JqueryUI().Dialog.Open(option =>
{
option.Resizable = false;
option.Title = "Add human";
});
}))
.AsHtmlAttributes()
.ToButton("Add new human"))
В реальных приложениях валидация введенных данных форм — одна из самых частых задач. Поэтому добавим валидацию данных на форму добавления/редактирования сущности Human. Первая часть — добавление серверного кода. Добавьте следующий код в AddOrEditHumanCommand как nested class:
#region Nested Classes
public class Validator : AbstractValidator
{
#region Constructors
public Validator()
{
RuleFor(r => r.FirstName).NotEmpty();
RuleFor(r => r.LastName).NotEmpty();
}
#endregion
}
#endregion
На форме AddOrEditHuman.cshtml мы использовали конструкции вида:<
@Html.ForGroup()
Поэтому нет необходимости дополнительно добавлять
@Html.ValidationMessageFor()
для полей — ForGroup() сделает это за нас.
Таким образом, мы написали код приложения, которое реализует CRUD-функционал для одной сущности БД.
Часть 4. Specifications — фильтрация данных
Еще одна из задач, которые часто встречаются в реальных проектах — фильтрация запрашиваемых данных. В Incoding Framework для удобства написания кода и соблюдения принципа инкапсуляции для фильтрации получаемых в Query данных используются WhereSpecifications. Добавим в написанный код возможность фильтрации получаемых в GetPeopleQuery данных по FirstName и LastName. В первую очередь добавим два файла спецификаций Example.Domain -> Specifications -> HumanByFirstNameWhereSpec.cs и Example.UI -> Specifications -> HumanByLastNameWhereSpec.cs
HumanByFirstNameWhereSpec.cs
namespace Example.Domain
{
#region << Using >>
using System;
using System.Linq.Exsource ssions;
using Incoding;
#endregion
public class HumanByFirstNameWhereSpec : Specification
{
#region Fields
readonly string firstName;
#endregion
#region Constructors
public HumanByFirstNameWhereSpec(string firstName)
{
this.firstName = firstName;
}
#endregion
public override Exsource ssion<Func<Human, bool>> IsSatisfiedBy()
{
if (string.IsNullOrEmpty(this.firstName))
return null;
return human => human.FirstName.ToLower().Contains(this.firstName.ToLower());
}
}
}
HumanByLastNameWhereSpec.cs
namespace Example.Domain
{
#region << Using >>
using System;
using System.Linq.Exsource ssions;
using Incoding;
#endregion
public class HumanByLastNameWhereSpec : Specification
{
#region Fields
readonly string lastName;
#endregion
#region Constructors
public HumanByLastNameWhereSpec(string lastName)
{
this.lastName = lastName.ToLower();
}
#endregion
public override Exsource ssion<Func<Human, bool>> IsSatisfiedBy()
{
if (string.IsNullOrEmpty(this.lastName))
return null;
return human => human.LastName.ToLower().Contains(this.lastName);
}
}
}
Теперь используем написанные спецификации в запросе GetPeopleQuery. При помощи связок .Or()/.And() атомарные спецификации можно соединять в более сложные, что помогает использовать созданные спецификации многократно и при их помощи тонко настраивать необходимые фильтры данных (в нашем примере мы используем связку .Or()).
GetPeopleQuery.cs
namespace Example.Domain
{
#region << Using >>
using System.Collections.Generic;
using System.Linq;
using Incoding.CQRS;
using Incoding.Extensions;
#endregion
public class GetPeopleQuery : QueryBase<List<GetPeopleQuery.Response>>
{
#region Properties
public string Keyword { get; set; }
#endregion
#region Nested Classes
public class Response
{
#region Properties
public string Birthday { get; set; }
public string FirstName { get; set; }
public string Id { get; set; }
public string LastName { get; set; }
public string Sex { get; set; }
#endregion
}
#endregion
protected override List<Response> ExecuteResult()
{
return Repository.Query(whereSpecification:new HumanByFirstNameWhereSpec(Keyword)
.Or(new HumanByLastNameWhereSpec(Keyword)))
.Select(human => new Response
{
Id = human.Id,
Birthday = human.Birthday.ToShortDateString(),
FirstName = human.FirstName,
LastName = human.LastName,
Sex = human.Sex.ToString()
})
.ToList();
}
}
}
И наконец, осталось немного модифицировать Index.cshtml, чтобы добавить поисковую строку, задействующую при запросе поле Keyword для фильтрации данных.
Index.cshtml
@using Example.Domain
@using Incoding.MetaLanguageContrib
@using Incoding.MvcContrib
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div id="dialog"></div>
@*При нажатии кнопки Find инициируется событие InitIncoding и PeopleTable выполняет запрос GetPeopleQuery с параметром Keyword*@
<div>
<input type="text" id="Keyword"/>
@(Html.When(JqueryBind.Click)
.Direct()
.OnSuccess(dsl => dsl.WithId("PeopleTable").Core().Trigger.Incoding())
.AsHtmlAttributes()
.ToButton("Find"))
</div>
@(Html.When(JqueryBind.InitIncoding)
.AjaxGet(Url.Dispatcher().Query(new GetPeopleQuery { Keyword = Selector.Jquery.Id("Keyword") }).AsJson())
.OnSuccess(dsl => dsl.Self().Core().Insert.WithTemplateByUrl(Url.Dispatcher().AsView("~/Views/Home/HumanTmpl.cshtml")).Html())
.AsHtmlAttributes(new { id = "PeopleTable" })
.ToDiv())
@(Html.When(JqueryBind.Click)
.AjaxGet(Url.Dispatcher().AsView("~/Views/Home/AddOrEditHuman.cshtml"))
.OnSuccess(dsl => dsl.WithId("dialog").Behaviors(inDsl =>
{
inDsl.Core().Insert.Html();
inDsl.JqueryUI().Dialog.Open(option =>
{
option.Resizable = false;
option.Title = "Add human";
});
}))
.AsHtmlAttributes()
.ToButton("Add new human"))
Часть 5. Юнит-тесты.
Покроем написанный код тестами. Первый тест отвечает за проверку маппинга сущности Human. Файл When_save_Human.cs добавим в папку Persisteces проекта UnitTests.
When_save_Human.cs
namespace Example.UnitTests.Persistences
{
#region << Using >>
using Example.Domain;
using Incoding.MSpecContrib;
using Machine.Specifications;
#endregion
[Subject(typeof(Human))]
public class When_save_Human : SpecWithPersistenceSpecification
{
#region Fields
It should_be_verify = () => persistenceSpecification.VerifyMappingAndSchema();
#endregion
}
}
Данный тест работает с тестовой базой данных (Example_test): создается экземпляр класса Human с автоматически заполненными полями, сохраняется в базу, а затем извлекается и сверяется с созданным экземпляром.
Теперь добавим тесты для WhereSpecifications в папку Specifications.
When_human_by_first_name.cs
namespace Example.UnitTests.Specifications
{
#region << Using >>
using System;
using System.Collections.Generic;
using System.Linq;
using Example.Domain;
using Incoding.MSpecContrib;
using Machine.Specifications;
#endregion
[Subject(typeof(HumanByFirstNameWhereSpec))]
public class When_human_by_first_name
{
#region Fields
Establish establish = () =>
{
Func<string, Human> createEntity = (firstName) =>
Pleasure.MockStrictAsObject(mock => mock.SetupGet(r => r.FirstName).Returns(firstName));
fakeCollection = Pleasure.ToQueryable(createEntity(Pleasure.Generator.TheSameString()),
createEntity(Pleasure.Generator.String()));
};
Because of = () =>
{
filterCollection = fakeCollection
.Where(new HumanByFirstNameWhereSpec(Pleasure.Generator.TheSameString()).IsSatisfiedBy())
.ToList();
};
It should_be_filter = () =>
{
filterCollection.Count.ShouldEqual(1);
filterCollection[0].FirstName.ShouldBeTheSameString();
};
#endregion
#region Establish value
static IQueryable fakeCollection;
static List filterCollection;
#endregion
}
}
When_human_by_last_name.cs
namespace Example.UnitTests.Specifications
{
#region << Using >>
using System;
using System.Collections.Generic;
using System.Linq;
using Example.Domain;
using Incoding.MSpecContrib;
using Machine.Specifications;
#endregion
[Subject(typeof(HumanByLastNameWhereSpec))]
public class When_human_by_last_name
{
#region Fields
Establish establish = () =>
{
Func<string, Human> createEntity = (lastName) =>
Pleasure.MockStrictAsObject(mock =>mock.SetupGet(r => r.LastName).Returns(lastName));
fakeCollection = Pleasure.ToQueryable(createEntity(Pleasure.Generator.TheSameString()),
createEntity(Pleasure.Generator.String()));
};
Because of = () =>
{
filterCollection = fakeCollection
.Where(new HumanByLastNameWhereSpec(Pleasure.Generator.TheSameString()).IsSatisfiedBy())
.ToList();
};
It should_be_filter = () =>
{
filterCollection.Count.ShouldEqual(1);
filterCollection[0].LastName.ShouldBeTheSameString();
};
#endregion
#region Establish value
static IQueryable fakeCollection;
static List filterCollection;
#endregion
}
}
Теперь осталось добавить тесты для команды и запроса (папка Operations), причем для команды необходимо добавить два теста: один для проверки создания новой сущности и второй для проверки редактирования уже существующей сущности.
When_get_people_query.cs
namespace Example.UnitTests.Operations
{
#region << Using >>
using System.Collections.Generic;
using Example.Domain;
using Incoding.Extensions;
using Incoding.MSpecContrib;
using Machine.Specifications;
#endregion
[Subject(typeof(GetPeopleQuery))]
public class When_get_people
{
#region Fields
Establish establish = () =>
{
var query = Pleasure.Generator.Invent<GetPeopleQuery>();
//Create entity for test with auto-generate
human = Pleasure.Generator.Invent<Human>();
expected = new List<GetPeopleQuery.Response>();
mockQuery = MockQuery<GetPeopleQuery, List<GetPeopleQuery.Response>>
.When(query)
//"Stub" on query to repository
.StubQuery(whereSpecification:
new HumanByFirstNameWhereSpec(query.Keyword)
.Or(new HumanByLastNameWhereSpec(query.Keyword)),
entities: human);
};
Because of = () => mockQuery.Original.Execute();
// Compare result
It should_be_result = () => mockQuery
.ShouldBeIsResult(list => list.ShouldEqualWeakEach(new List<Human>() { human }, (dsl, i) =>
dsl.ForwardToValue(r => r.Birthday, human.Birthday.ToShortDateString())
.ForwardToValue(r => r.Sex, human.Sex.ToString())));
#endregion
#region Establish value
static MockMessage<GetPeopleQuery, List<GetPeopleQuery.Response>> mockQuery;
static List<GetPeopleQuery.Response> expected;
static Human human;
#endregion
}
}
When_add_human.cs
namespace Example.UnitTests.Operations
{
#region << Using >>
using Example.Domain;
using Incoding.MSpecContrib;
using Machine.Specifications;
#endregion
[Subject(typeof(AddOrEditHumanCommand))]
public class When_add_human
{
#region Fields
Establish establish = () =>
{
var command = Pleasure.Generator.Invent<AddOrEditHumanCommand>();
mockCommand = MockCommand<AddOrEditHumanCommand>
.When(command)
//"Stub" on repository
.StubGetById<Human>(command.Id, null);
};
Because of = () => mockCommand.Original.Execute();
It should_be_saved = () => mockCommand
.ShouldBeSaveOrUpdate<Human>(human => human.ShouldEqualWeak(mockCommand.Original));
#endregion
#region Establish value
static MockMessage<AddOrEditHumanCommand, object> mockCommand;
#endregion
}
}
When_edit_human.cs
namespace Example.UnitTests.Operations
{
#region << Using >>
using Example.Domain;
using Incoding.MSpecContrib;
using Machine.Specifications;
#endregion
[Subject(typeof(AddOrEditHumanCommand))]
public class When_edit_human
{
#region Fields
Establish establish = () =>
{
var command = Pleasure.Generator.Invent<AddOrEditHumanCommand>();
human = Pleasure.Generator.Invent<Human>();
mockCommand = MockCommand<AddOrEditHumanCommand>
.When(command)
//"Stub" on repository
.StubGetById(command.Id, human);
};
Because of = () => mockCommand.Original.Execute();
It should_be_saved = () => mockCommand
.ShouldBeSaveOrUpdate<Human>(human => human.ShouldEqualWeak(mockCommand.Original));
#endregion
#region Establish value
static MockMessage<AddOrEditHumanCommand, object> mockCommand;
static Human human;
#endregion
}
}
Список материалов для изучения
- CQRS — архитектура серверной части
- MVD — описание паттерна Model View Dispatcher
- IML introduction
- IML TODO MVC
Комментарии (43)
ZOXEXIVO
15.09.2015 18:20-2А собственно зачем это тут? Мало чтоли фреймворков? Кто будет это использовать?
Кому нужно изучать странный способ определения банальных вещей (это я больше про *.cshtml)?
vkopachinsky
15.09.2015 18:56А собственно зачем это тут?
Решил, что это удачное место для продвижения
Мало чтоли фреймворков?
Одновременно client/server, не много.
Кто будет это использовать?
После подробного руководства, начинающие программисты (asp.net mvc), а тех кто уже пишет на чем то, будет сложно «переманить»
Кому нужно изучать странный способ определения банальных вещей (это я больше про *.cshtml)?
Основная причина, это типизация, которой так не хватает на JS.
P.S. Статья (как и сам framework) была в первую очередь для сотрудников нашей компании, а тут в целях информировать о том что у нас получилось (мы считаем, что не плохо) и попытки развить его во что то большое.lair
15.09.2015 18:59Основная причина, это типизация, которой так не хватает на JS.
TypeScript?vkopachinsky
15.09.2015 19:19TypeScript?
Это не framework, а язык, так что это сложно сравнивать.lair
15.09.2015 19:20Так и JS — язык. Типизация вообще свойственна языку, а не фрейворку. Если вам не хватает типизации — почему не взять язык, в котором она есть, зачем городить фреймворк?
vkopachinsky
15.09.2015 19:32Так и JS — язык.
Я думал, Вы в рамках framework сравниваете, а так да, Вы правы.
Если вам не хватает типизации — почему не взять язык, в котором она есть, зачем городить фреймворк?
TypeScript не имеет интеграции с template (где у нас тоже типизация), а в сравнение с IML (декларативный язык) он не имеет функционала. Framework нужен, что бы ускорить разработку за счет «абстракции» от рутинных операций (Repository, CQRS, Ajax и т.д.), которые повторяются из проекта в проект.
Типизация на всех этапах это один из бонусов, но основное, что мы получаем от incframework — это эко-систему для разработки приложений (web,desktop). А вот на сколько гибкую, это пока открытый вопрос.
P.S. Мы уже вели дискуссии на эту тему, так что мне кажется Вам должны быть знакомы мои ответы ))
lair
15.09.2015 21:05Я думал, Вы в рамках framework сравниваете, а так да, Вы правы.
А я не сравниваю, я отвечаю на ваш же тезис о том, что основная причина того, что вы напридумывали во view — это отсутствие типизации в JS. Как можно видеть из вашего дальнейшего текста, вы слегка лукавите.vkopachinsky
15.09.2015 21:24А я не сравниваю, я отвечаю на ваш же тезис о том
Тезиз был в сравнение TypeScript и Incoding Framework.
что основная причина того, что вы напридумывали во view — это отсутствие типизации в JS
Да, в рамках View кроме поддержки Intelisense была задача получить декларативный язык, который будет интегрирован с серверной частью приложения (вызов CQRS) и типизированные Templatе и конечно больше ни каких undifend ))
В итоге мы пришли к тому, что все приложения строится на языке C# без написания дополнительного JS кода.
Плохо это или нет, уже конечно каждый сам решает, но многие скажут, что лучше знакомый JS, чем неизвестный псевдо-язык (просто набор методов на C# аля domain specific language) хотя мы на своей практики получили огромный прирост по скорости и безопасности при разработке.
— Скорость заключается в таких возможностях, как оборачивать код в HTML extensions (пример сложного), а так же Intellisense.
— Безопасность находится в том, что IML язык декларативный, а это значит то, что вызов любого метода будет работать (при условии правильных параметров), если конечно нету глобальной ошибки в рамках самого framework. Да это накладывает ограничения (есть много вариантов, для работы со сторонним JS), но всему своя цена.
На критику, что любой метод/код можно написать рабочим, скажу то что его надо реализовывать, а IML (к примеру HTML декларативный язык) уже готов и проверен.
P.S. Конечно IML это просто набор методов на языке C#, так что назвать его по «настоящему» декларативным сложно, но мне кажется это понятие очень хорошо подходит для его описания.lair
15.09.2015 21:26Тезиз был в сравнение TypeScript и Incoding Framework.
И кто придумал это сравнение?
В итоге мы пришли к тому, что все приложения строится на языке C# без написания дополнительного JS кода.
Я даже знаю, где я это уже видел — в WebForms. Там тоже серверные компоненты позволяли построить весь интерфейс декларативно…vkopachinsky
15.09.2015 21:32Я даже знаю, где я это уже видел — в WebForms. Там тоже серверные компоненты позволяли построить весь интерфейс декларативно…
Так же можно привести в пример и asp.net mvc (Html.TextBoxFor, Html.DropDownFor) Если, Вы смотрели, как мы интегрируем IML в html элементы, то заметили, что используется RouteValueDictionary, который передается в TextBoxFor,DropDownFor и т.д.
Основное преимущество TextBoxFor и других html extensions, в том что их можно «связать» с серверной часть, а в частности с моделью.lair
15.09.2015 21:37Так же можно привести в пример и asp.net mvc (Html.TextBoxFor, Html.DropDownFor)
Отнюдь. asp.net MVC как раз спроектирован так, чтобы максимально облегчить взаимодействие HTML и клиентских скриптов (а не заменить одно другим). Это неоднократно повторяется в соответствующей литературе (например, у Эспозито): asp.net MVC опускается на уровень (или несколько) абстракции ниже, чем WebForms, чтобы дать фронт-энд разработчикам более детальный контроль за происходящим.
А вы поднимаете этот уровень абстракции обратно. Хорошо это или плохо — вопрос отдельный.
PS.AjaxGet(Url.Dispatcher().AsView("~/Views/Home/AddOrEditHuman.cshtml"))
Здравствуй, нарушение SoC.vkopachinsky
15.09.2015 21:49А вы поднимаете этот уровень абстракции обратно.
Мы просто дополнительно передаем атрибуты в качестве параметра в уже существующие «конторолы» asp.net mvc.
Вы считаете, что возможность написать свой (ниже пример) html extensions это плохо?
@Html.Rap().Admin.Tab(setting => { setting.Url = Url.Dispatcher().AsView("~/Views/DiabetesMedication/Index.cshtml"); setting.Title = "Diabetes Medications"; })
Сравнивания с asp.net control такие как, updatepanel, grid и т.д, то надо понимать, что для обработке клиентских сценариев все равно требовалось подписывать на события и писать JS, а мы полностью строим все на сервере (а вот выполняем и там и там)
Вот пример, ajax drop down в виде готового control
@Html.ForGroup(r => r.FirstName).TextBox(control => control.Label.Name = "First name")
пример из статьи
Здравствуй, нарушение SoC.
Попробовал загуглить, но не нашел определения для SoC, Вы имеете ввиду, какую то проблему с безопасностью?vkopachinsky
15.09.2015 21:54UPD. Пример, не тот привел, вот для Ajax drop down
@Html.For(r => r.AgencyId).DropDown(control => { control.Url = Url.Dispatcher().Query<GetSupportServicesForDDQuery>().AsJson(); control.AddClass("selectInput400 firstControl"); })
lair
15.09.2015 21:55Мы просто дополнительно передаем атрибуты в качестве параметра в уже существующие «конторолы» asp.net mvc.
Окей, давайте на примере.
Вы говорите, что ваше приложение строится целиком на C#, без написания JS. Как вы реализуете сложную клиентскую валидацию (поле такое-то должно зависеть от полей таких-то и таких-то)? А динамические интерфейсы (если выбрано поле такое-то, показать поля такие-то и такие-то)?
Сравнивания с asp.net control такие как, updatepanel, grid и т.д, то надо понимать, что для обработке клиентских сценариев все равно требовалось подписывать на события и писать JS, а мы полностью строим все на сервере (а вот выполняем и там и там)
Я сравниваю с девэкспрессом, где чертова уйма клиентского поведения реализовывалась самим контролом, и была разработчику недоступна.
Попробовал загуглить, но не нашел определения для SoC, Вы имеете ввиду, какую то проблему с безопасностью?
Я имею в виду Separation of Concerns.vkopachinsky
15.09.2015 22:04Я имею в виду Separation of Concerns.
AjaxGet(Url.Dispatcher().AsView("~/Views/Home/AddOrEditHuman.cshtml"))
это тоже самое, что и
AjaxGet(Url.Action(«AddOrEditHuman»,«Home»))
Так что, не совсем понял, что именно нарушается. MVD (Url.Dispatcher()), просто строит адрес, но выполняется все потом на сервере в рамках Command/Query и Dispatcher
Я сравниваю с девэкспрессом, где чертова уйма клиентского поведения реализовывалась самим контролом, и была разработчику недоступна.
Вы сами пишете это поведение, точно так же, как и страницу HTML с помощью тэгов, но не думаете, как они «рисуются» браузером
P.S. Ответил на быстрые вопросы, а про валидацию с примерами, чуть позже.lair
15.09.2015 22:07AjaxGet(Url.Dispatcher().AsView("~/Views/Home/AddOrEditHuman.cshtml"))
это тоже самое, что и
AjaxGet(Url.Action(«AddOrEditHuman»,«Home»))
Нет. В первом случае я явно указываю серверу, какое представление мне нужно, а во втором — я указываю, какой экшн у какого контроллера вызвать. Разная степень информированности о сервере.
Вы сами пишете это поведение, точно так же, как и страницу HTML с помощью тэгов, но не думаете, как они «рисуются» браузером
Вот это я и называю повышеним абстракции — рисование, не думая. В WebForms действительно была такая идеология. В asp.net MVC от нее отказались.vkopachinsky
15.09.2015 22:12Нет. В первом случае я явно указываю серверу, какое представление мне нужно, а во втором — я указываю, какой экшн у какого контроллера вызвать. Разная степень информированности о сервере.
Мы уже долго говорили на эту тему в статье про MVD. Мне кажется это не нарушение Separation of Concerns, а больше похоже на «обобщение», хотя SuperService тоже на него похож, но…
По мне плюсы, которые я получаю, а именно возможность писать на много меньше кода, покрывают с лихвой это момент, при том условии, что Вам не нужно ничего дополнительно дописывать.
Вот это я и называю повышеним абстракции — рисование, не думая. В WebForms действительно была такая идеология. В asp.net MVC от нее отказались.
Но, так же используют Html extensions для скрытия сложных и часто повторяемых html конструкций. Мы тоже довольно низко-уровневая часть, то есть IML он просто дает функционал, но ничего конкретного не реализовывает к примеру grid, tabs или TextBoxAlertByChangelair
15.09.2015 22:14По мне плюсы, которые я получаю, а именно возможность писать на много меньше кода, покрывают с лихвой это момент, при том условии, что Вам не нужно ничего дополнительно дописывать.
К сожалению, почти всегда — надо.
Мы тоже довольно низко-уровневая часть, то есть IML он просто дает функционал, но ничего конкретного не реализовывает к примеру grid, tabs или TextBoxAlertByChange
Тогда как же вам удалось полностью отказаться от JS?vkopachinsky
15.09.2015 22:20К сожалению, почти всегда — надо.
MVD умеет вызывать Command/Query, отреднерить View (с Model или с данными из query), получить JSON или File из Query, что ещё надо даже не знаю. Если по каким то причинам, будет не хватать возможностей (хотя я пока не вижу), то можно совместно использовать и обычный Controller ( на работу AjaxGet это не повлияет, потому что ему нужен просто URL к вашем данным)
Тогда как же вам удалось полностью отказаться от JS?
от написания ДОПОЛНИТЕЛЬНОГО кода, то есть сам движок, который выполняет IML само собой на JS, потому что browser особо ничего не понимает.
P.S. примеры по валидации сейчас пишу!lair
15.09.2015 22:21сам движок, который выполняет IML само собой на JS
… и этот JS программистом никак не контролируется. Вот именно это мне и напомнило старые добрые времена WebForms.vkopachinsky
15.09.2015 22:23… и этот JS программистом никак не контролируется. Вот именно это мне и напомнило старые добрые времена WebForms.
Нету у нас JS программистов ))))
Каждый разработчик это front-end и back-end в одном лице, потому что после написания Command/Query, он может сразу её выполнить на JS, не используя его.lair
15.09.2015 22:24Нету у нас JS программистов
Оно и видно.vkopachinsky
15.09.2015 22:25Оно и видно.
Вы я так понимаю, начинаете грубить?lair
15.09.2015 22:26Нет, почему же. Просто это объясняет ваше желание полностью убрать программирование на JS.
vkopachinsky
15.09.2015 22:30Нет, почему же. Просто это объясняет ваше желание полностью убрать программирование на JS.
Хм, фраза «оно и видно» чаще намекает, на незнание или подчеркивание плачевности ситуации оппонента в каком то деле. Но, раз Вы этого не имели ввиду, то все окей.
P.S. примеры по валидаци, пишу ))
vkopachinsky
16.09.2015 00:14Вы говорите, что ваше приложение строится целиком на C#, без написания JS. Как вы реализуете сложную клиентскую валидацию (поле такое-то должно зависеть от полей таких-то и таких-то)? А динамические интерфейсы (если выбрано поле такое-то, показать поля такие-то и такие-то)?
Сценарий проверки email на уникальность по Ajax
<span class="field-valiion-valid"></span> @(Html.When(JqueryBind.Change) .AjaxGet(Url.Dispatcher().Query(new IsEmailUniqueQuery() { Email = Selector.Jquery.Self() })) .OnSuccess(dsl => { dsl.WithClass("field-valiion-valid").JQuery.Attr.RemoveClass("field-validation-error"); dsl.WithClass("field-valiion-valid").JQuery.Attr .AddClass("field-validation-error") .If(() => Selector.Result.For<IncBoolResponse>(r => !r.Value)); }) .AsHtmlAttributes(new {id ="id or you can use TextBoxFor with typed C#"}) .ToTextBox())
примечание: проверка идет на клиенте, но ещё можно (на много легче), валидировать на сервер и далее OnError(r=>r.Self().Form.Validation.Refresh()), который интегрирован с ModelState (в статье есть пример)
ещё примечание: если у Вас часто идет проверка по Ajax, то можно обернуть в Html.ProjName().Control.Unique(r=>r.Email)
Валидатора для предыдущего примера
public class Validator : AbstractValidator<TCommand> { public Validator() { var dispatcher = IoCFactory.Instance.TryResolve<IDispatcher>(); RuleFor(r => r.Email) .Must((email) => dispatcher.Query(new IsEmailUniqueQuery() { Email = email })); } }
примечание: мы повторно используем код
Сценарий зависимых элементов
@model FirstAndLastName @Html.CheckBoxFor(r => r.SomethingElse) @Html.For(r => r.First).TextBox(control => { control.OnChange = dsl => dsl.With(Html.Selector().Name(r => r.Last)) .JQuery.Attr.AddClass("hide") .If(() => Selector.Jquery.Self() == "Remove hide from Last" && Html.Selector().Name(r => r.SomethingElse)); }) @Html.For(r => r.Last).TextBox()
IML позволяет строить сложные условия или к примеру, установить значения одного элемента из другово (и не только html объекта)
dsl.With(someSelector).Jquery.Attr.Val(Selector.Incoding.QueryString("test"))
P.S. Можно оценить возможности IML на примере todo mvc, а так же есть статьи о template, selector и т.д. Раньше была «песочница», но после редизайна сайта, не было времени её востановить.lair
16.09.2015 11:33Cценарий проверки email на уникальность по Ajax
Это ужасно. Нет, это серьезно ужасно.
Во-первых, это уродливо. Ваш DSL очень плохо читается. Нет, правда:Html.When().AjaxGet().OnSuccess().AsHtmlAttributes().ToTextBox()
. Событие и цель события разнесены в разные концы, смысл двух последних действий из названия не понятен.
Во-вторых, это семантически неверно. В вашем примере не валидация, а произвольный код, где-то ссылающийся на JQuery. Ничто, кроме классов, не указывает на то, что это валидация, нет очевидной связи с общей отправкой формы. И это несмотря на то, что есть JQuery Validation, который все это умееет.
Ну и в-третьих, это неверный уровень абстрации. Вот как это должно выглядеть на самом деле:
class SomeModel { [ValidationQuery(typeof(IsEmailUniqueQuery)] public string Email {get; set;} }
Ну или ad-hoc:
Html.TextBoxFor(m => m.Email).Validate(v => v.WithQuery<IsEmailUniqueQuery>())
Сценарий зависимых элементов
Тут все лучше, но нет никакой возможности оптимизировать получившийся JS-код… впрочем, как мы выясняли, у вас это некому делать.
(повторяться про то, что сервис-локатор — зло, я не буду)vkopachinsky
16.09.2015 12:07Ну и в-третьих, это неверный уровень абстрации. Вот как это должно выглядеть на самом деле:
То, что Вы привели это будет работать в первую очередь на сервер, на клиент надо дописывать Jquery validation и т.д.
Ну или ad-hoc:
Если Вы читали, то увидели, что я привел такой пример «Html.ProjName().Control.Unique(r=>r.Email)»
нет очевидной связи с общей отправкой формы
Проверка email идет сразу после ввода, если Вам нужно после в моменты отправки form, то в incoding framework можно написать validator и в обработчике Submit добавить OnError в котором вызывать Form.Validation.Refresh().
IML позволяет работать с клиентской частью, а то как в какие контролы или элементы будет обернут код, не столь важно.
Тут все лучше, но нет никакой возможности оптимизировать получившийся JS-код
Это примерно, как Nhibernate (или другой ORM), там все уже оптимизировали и своего добавить нельзя, но зато можно не писать SQL
впрочем, как мы выясняли, у вас это некому делать.
Все же грубите ?)lair
16.09.2015 12:15То, что Вы привели это будет работать в первую очередь на сервер, на клиент надо дописывать Jquery validation и т.д.
Нет, это будет работать там, куда вы это напишете. MVC, например, умеет генерить клиентскую валидацию из атрибутов.
Если Вы читали, то увидели, что я привел такой пример «Html.ProjName().Control.Unique(r=>r.Email)»
Это поддерживается фреймворком или написанный программистом хелпер? Ну и он все равно семантически неверен.
Проверка email идет сразу после ввода, если Вам нужно после в моменты отправки form, то в incoding framework можно написать validator и в обработчике Submit добавить OnError в котором вызывать Form.Validation.Refresh().
… а все это должно работать само и целиком. Как, собственно, в MVC и работает.
Это примерно, как Nhibernate (или другой ORM), там все уже оптимизировали и своего добавить нельзя, но зато можно не писать SQL
Про точки расширения в ORM вы не слышали? И да, ORM — это потеря производительности и контроля, и есть проекты, где их ровно поэтому не применяют.
Но главное — все-таки под ORM уже лежит декларативная парадигма, а вы пытаетесь заменить императивный язык неким метаскриптом. Пропасть глубже.
Все же грубите ?)
Нет, аргументирую для себя принятые вами решения.vkopachinsky
16.09.2015 12:41Нет, это будет работать там, куда вы это напишете. MVC, например, умеет генерить клиентскую валидацию из атрибутов.
Понятно дело, что fluent validation прекрасно транслирует NotEmpty,NotNull, но к примеру Must уже не получится, а вызов Ajax это тот самый случай.
Это поддерживается фреймворком или написанный программистом хелпер? Ну и он все равно семантически неверен.
В framework по мимо IML, есть и набор готовых решений на нем, но для каждого проекта присущи свои особенности.Задача была написать, как язык для решения любых сценариев, так и готовые компоненты.
… а все это должно работать само и целиком. Как, собственно, в MVC и работает.
Это разные сценарии, иногда надо отправить форму и провалидоравать, но так же есть ситуации быстрого реагирования на действия пользователей.
Нет, аргументирую для себя принятые вами решения.
Ваш профиль говорит, о том что кроме как «аргументирую для себя принятые кем то решения» не делаете ничего. Да, осуждать можно и не показав свои примеры работ, но Вы это делаете постоянно и я думаю было бы справедливо предоставить плоды Вашей деятельности?lair
16.09.2015 12:54Понятно дело, что fluent validation прекрасно транслирует NotEmpty,NotNull, но к примеру Must уже не получится, а вызов Ajax это тот самый случай.
Что вам мешает самим-то (внутри фреймворка) реализовать аналогичное? Это же тривиальный анализ мета-данных и эмит кода в представление.
В framework по мимо IML, есть и набор готовых решений на нем, но для каждого проекта присущи свои особенности.Задача была написать, как язык для решения любых сценариев, так и готовые компоненты.
Ну то есть программистом. Что, знаете ли, удивительно, учитывая, как много вы говорите о своем фреймворке с точки зрения «уменьшает количество кода». Валидация — один из первых пунктов интереса в таком фреймворке (и именно она съедает ощутимую долю усилий).
Это разные сценарии, иногда надо отправить форму и провалидоравать, но так же есть ситуации быстрого реагирования на действия пользователей.
Хороший валидационный фреймворк делает и то, и другое, и без дополнительных усилий со стороны пользователя.
Ваш профиль говорит, о том что кроме как «аргументирую для себя принятые кем то решения» не делаете ничего.
Странно, я думал, что в нем есть ссылка на мой фейсбук, в котором есть все, что надо знать о моей работе (к сожалению, по непонятным причинам хабр не делает ссылок на LinkedIn или, что веселее, StackOverflow, но они тоже легко находятся).
я думаю было бы справедливо предоставить плоды Вашей деятельности?
Нет, спасибо, на «сперва добейся» я не ведусь — особенно учитывая, что моя деятельность в конкретной обсуждаемой области была для конкретного работодателя, и, как следствие, код я вам показать не смогу.vkopachinsky
16.09.2015 13:02Что вам мешает самим-то (внутри фреймворка) реализовать аналогичное? Это же тривиальный анализ мета-данных и эмит кода в представление.
Как Вы можете рассуждать, что надо реализовать, если Вы даже не использовали?
Ну то есть программистом. Что, знаете ли, удивительно, учитывая, как много вы говорите о своем фреймворке с точки зрения «уменьшает количество кода». Валидация — один из первых пунктов интереса в таком фреймворке (и именно она съедает ощутимую долю усилий).
Вы не хотите ничего читать, в статье (и много другого материала об этом) есть описание, как работает валидация
.OnError(dsl => dsl.Self().Core().Form.Validation.Refresh())
Нет, спасибо, на «сперва добейся» я не ведусь — особенно учитывая, что моя деятельность в конкретной обсуждаемой области была для конкретного работодателя, и, как следствие, код я вам показать не смогу.
Вы очень долго уже критикуете меня, так что я считаю было бы справедливо показать, как надо? На счет «не могу показать кода», то есть для себя Вы не пишите ни каких инструментов, которые используете от проекта в проекту?
P.S. До сих пор не понятен Ваш интерес, потому что сам framework Вам не нужен (мы говорили о нем год назад, но я так понимаю Вы его даже не устанавливали). Спорить с Вами очень трудно и я думаю доказать, что то не возможно, но тот факт, что статья продвигает из-за кол-во комментариев меня побуждает продолжать дискуссию )lair
16.09.2015 13:07Как Вы можете рассуждать, что надо реализовать, если Вы даже не использовали?
Легко — я посмотрел на приведенный вами код. Да, я могу ошибаться в том, насколько это сложно сделать внутри вашего конкретного кода, но я знаю, что это несложно сделать в ванильном asp.net MVC.
Вы не хотите ничего читать, в статье (и много другого материала об этом) есть описание, как работает валидация
Во-первых, лишний вызов (валидация должна работать прозрачно). Во-вторых, это клиентская валидация или серверная?
то есть для себя Вы не пишите ни каких инструментов, которые используете от проекта в проекту?
Обычно я их пишу для проектов работодателя, и там же они и остаются. То, что нужно мне самому, чаще всего уже есть в том или ином открытом проекте, куда я при случае контрибьючу. BTW, пример такой контрибуции тоже легко находим через мой профиль, который вы, по вашим словам, посмотрели.vkopachinsky
16.09.2015 13:25Во-первых, лишний вызов (валидация должна работать прозрачно). Во-вторых, это клиентская валидация или серверная?
Если клиентская транслируется (к примеру Fluent validation), то будет на клиенте (и сервер), иначе отправляем (через ajax) сервер и в случаи не удачи (не критической http 500, а логической) то вернем IncodingResult.Error(ModelState), который в OnError обрабатываем, как угодно.
К примеру, можем кастомно сделать dsl.WithId(«someId»).Insert.WithTemplateByUrl(«path to tempate for model state»).Html() или же dsl.WithTag(Form).Form.Validation.Refresh(), что приведет к обновлению валидационных сообщений.
BTW, пример такой контрибуции тоже легко находим через мой профиль, который вы, по вашим словам, посмотрели.
Я не увидел ссылок в Вашем профиле, так что если хотите, то укажите тут.
vkopachinsky
18.09.2015 20:18Что бы показать уровень интеграции, создал на github пример.
Вкратце, можно выделить 3 типа валидации:
- Клиентская и Серверная — RuleFor(r=>r.NotEmpty).NotEmpty() может выполнятся на клиенте (jquery validate
примечание: можно дописать свой вариант трансляции в рамках fluent validation, в том числе Ваш ValidationQuery(typeof(IsEmailUniqueQuery))
- Серверная — Must или When, которые не могут быть выполнены на клиенте, то выполняются на сервере а потом в качестве ModelState возвращаются на форму (с точки зрения пользователя разница не существенная)
- Серверная в рамках Command — Бывают ситуации, когда остановить выполнение нужно в середине процесса, то в этом случаи Incoding Framework позволяет «выбрасывать» специальный exception, который будет работать так же как и Must (fluent validation)
Примеры
Command
public class TestCommand : CommandBase { public string NotEmpty { get; set; } public string Must { get; set; } public string ThrowFromCommand { get; set; } public override void Execute() { throw IncWebException.For<TestCommand>(command => command.ThrowFromCommand, "Error"); } public class Validator : AbstractValidator<TestCommand> { public Validator() { RuleFor(r => r.NotEmpty).NotEmpty(); RuleFor(r => r.Must).Must(s => s != "Test").WithMessage("Value it is 'Test'"); } } }
View
@using (Html.BeginPush(setting => { setting.OnSuccess = dsl => dsl.Utilities.Window.Alert("Success"); })) { // work on client (also checking on server) because can translated by Fluent Validation @Html.ForGroup(r => r.NotEmpty).TextBox(control => { control.Label.Name = "Not Empty"; }) // work ONLY server side @Html.ForGroup(r => r.Must).TextBox(control => { control.Label.Name = "Value"; }) // work ONLY server side @Html.ForGroup(r => r.ThrowFromCommand).TextBox(control => { control.Label.Name = "Error"; }) <input type="submit" value="Submit"/> }
Html extensions
public static class HtmlExtensions { public static IDisposable BeginPush<T>(this HtmlHelper<T> htmlHelper, Action<Setting> action) { var setting = new Setting(); action(setting); var url = new UrlHelper(htmlHelper.ViewContext.RequestContext); return htmlHelper.When(JqueryBind.Submit) .PreventDefault() .Submit() .OnBegin(dsl => dsl.Self().Core().Form.Validation.Parse()) .OnSuccess(dsl => { if (setting.OnSuccess != null) setting.OnSuccess(dsl); }) .OnError(dsl => dsl.Self().Core().Form.Validation.Refresh()) .AsHtmlAttributes() .ToBeginForm(htmlHelper, url.Dispatcher().Push(new TestCommand())); } public class Setting { public Action<IIncodingMetaLanguageCallbackBodyDsl> OnSuccess { get; set; } } }
- Клиентская и Серверная — RuleFor(r=>r.NotEmpty).NotEmpty() может выполнятся на клиенте (jquery validate
timramone
16.09.2015 00:35TypeScript не имеет интеграции с template
а вот и имеет. называется tsx :)vkopachinsky
16.09.2015 00:44а вот и имеет. называется tsx :)
TypeScripts new support for JSX — это просто библиотека, которую теперь можно использовать в TypeScript, с тем же успехом можно и handlebars или mustaches, но я говорил о более тесной интеграции ( пример с github)
dsl.Self().WithTemplateUrl(Url.Dispatcher().AsView("path")).Html()
То есть, template это часть инфраструктуры framework.
ZOXEXIVO
15.09.2015 19:18+1Получилось то может и неплохо, просто обычно все фреймворки пишут для своих нужд и когда человек будет пытаться адаптировать все это под свои (не в HelloWord), то возникнут проблемы, иногда не решаемые и тогда начнется…
vkopachinsky
15.09.2015 19:47и когда человек будет пытаться адаптировать все это под свои (не в HelloWord), то возникнут проблемы, иногда не решаемые и тогда начнется…
Да, Вы правы такое бывает, но framework разрабатывался (и до сих пор развивается) для разных проектов, потому что наша компания занимается outsource, а это приводит к постоянной смене направления (от досок объявлений до медицинских порталов). По этому я могу с уверенностью сказать, что гибкость достигается за счет IoC (замена IRepository, IUnitOfWork и т.д.), а так же на клиенте Вы можете выбрать Template Engine или взаимодействовать со сторонним JS кодом ( jquery plugin и т.д.).
Если, не сложно то назовите несколько ситуаций, когда Вы упирались в ограничения какого-либо framework. Я спрашиваю, что бы постараться привести наш пример решения, то или иной задачи в рамках incoding framework
P.S. Я не утверждаю, что у нас все продуманно и идеально (такое наверно не бывает), но и framework для «одной задачи» мы не делали.
xGromMx
Помню твои статьи против Angular))) Уже не воюешь теперь?)
vkopachinsky
Приятно видеть, что помнишь ))
Я думаю, это утрирования, потому что в статье было сравнение (где я выделял наши плюсы), может конечно картинка была воинственно настроена. Скажем так, изначально я начал не правильно описывать, но теперь решил исправится ))
xGromMx
Как тогда ни одного комментария в коде, так и сейчас. Вы хоть бы код комментариями покрыли. Да и если честно код не ахти.
vkopachinsky
Почему же ни одного, блочные комментарии есть, а каждую строку показалось очень перегружено и по ширине не удобно читать.
Буду признателен, если посоветуете, что то конкретное. Это Get started, где я не рассматривал моменты, обобщенные Command/Query или html extensions, но и «не ахти» я не не вижу, хотя конечно это моё мнение.