Breeze.js + Entity Framework + Angular.js = зручна робота з сутностями бази даних прямо з браузера

    

Деякий час тому на просторах мережі зіткнувся з цікавою бібліотекою Breeze.js. Перша думка, яка прийшла на думку при погляді на неї: «Так це ж як Entity Framework для браузера». У пошуках інформації та відгуків інших користувачів, звичайно, першим ділом пошукав статтю на Хабре, але не знайшов, тому і вирішив написати, в надії, що комусь це буде корисним. Стаття написана у вигляді tutorial зі створення проекту на основі Breeze.js, Angular.js, ASP.NET Web API і Entity Framework.

Аналогія з Entity Framework виникла тому, що основна робота з бібліотекою відбувається за допомогою класу EntityManager, він зберігає модель даних, реалізує їх отримання, стежить за змінами і дозволяє їх зберігати, ніж і трохи нагадує DbContext. Крім цього він надає ще ряд цікавих можливостей, наприклад валідацію на клієнтській і серверній стороні, кешування даних в браузері, експорт даних для збереження у тимчасовому сховищі (наприклад, при втраті зв'язку) і імпорт цих даних для подальшої синхронізації з сервером.

Клієнтська частина бібліотеки реалізує роботу по протоколу OData, тому Ви не зобов'язані використовувати якісь конкретні бібліотеки для реалізації backend'а. Але команда Breeze.js надає також бібліотеки для швидкого створення сервісів на основі наступних технологій:

У цій статті ми будемо створювати backend за допомогою WebApi + EF.

Для роботи EntityManager потребує метаданих сутностей, з якими Ви плануєте працювати, їх зв'язків і правил валідації(якщо Ви плануєте тільки запитувати дані, то можна обійтися і без метаданих). Їх можна імпортувати з спеціального об'єкта на клієнті, сформувати за допомогою виклику відповідних методів на клієнті, або використовувати найбільш простий спосіб — це отримання метаданих з DbContext або ObjectContext Entity Framework з допомогою класу EFContextProvider<>. При цьому потрібно розуміти, що вся схема даних, стає доступна на клієнта, і, якщо Ви не хочете розкривати схему повністю і використовувати для доступу до даних DTO, то доведеться створити спеціальний контекст, який буде служити тільки для спрощення формування потрібних Вам метаданих.

Мабуть, наочніше буде перейти до практики. У нашому прикладі будемо використовувати Entity Framework для доступу до бази даних, WebApi контролер зі спеціальним атрибутом BreezeController як backend, Angular.js як основу frontend, ну і звичайно ж Breeze.js для доступу до даних з браузера. ASP.NET MVC використовувати не будемо, так як розмітку нам буде будувати в браузері Angular.js він ж подбає і про роутинге. Тим більше, не за горами ASP.NET vNext, перейти на який буде значно простіше, якщо ми використовуємо лише WebApi, який не прив'язаний до IIS, на відміну від MVC.

Отже, поїхали. Запускаємо Visual Studio 2013 будь-редакції і створюємо новий порожній проект ASP.NET. Заходимо в NuGet і встановлюємо наступні пакети та їх залежності: EntityFramework, Breeze Client and Server — client with Javascript ASP.NET Web API 2 and Entity Framework 6 (це пакет, зібраний з кількох потрібних нам + він потягне за собою WebAPI), Breeze Labs: Breeze Angular Service, Angular JS, ну і куди ж без Bootstrap, і можна ще для краси додати angular-loading-bar. Далі переходимо на вкладку Updates і натискаємо Update All. Так ми отримали всі свіжі версії потрібних нам пакетів. Тут ми встановили все що потрібно з NuGet, але, на мій смак, куди зручніше все frontend бібліотеки встановлювати за допомогою Bower, якщо Вам цікаво дізнатися більше про те, як можна зручно використовувати Npm, Bower, Gulp і Grunt в Visual Studio, ось є переклад статті на цю тему.

Модель
Наше додаток буде представляти із себе список покупок, де всі товари будуть розкладені за категоріями, щоб розглянути роботу зі зв'язками. Для початку створимо класи моделей даних, використовуючи підхід Code First, і будемо класти їх у папку Models (це ім'я використовується за угодою, але на практиці можна класти куди завгодно):

Клас ListItem буде представляти елемент нашого списку
public class ListItem 
{ 
public int Id { get; set; } 
public String Name { get; set; } 
public Boolean IsBought { get; set; } 
public int CategoryId { get; set; } 
public Category Category { get; set; } 
} 

Клас Category — це категорія, до якої належить елемент
public class Category 
{ 
public int Id { get; set; } 
public String Name { get; set; } 
public List<ListItem> ListItems { get; set; } 
} 

Створюємо DbContext
public class ShoppingListDbContext: DbContext 
{ 
public DbSet<ListItem> ListItems { get; set; } 
public DbSet<Category> Categories { get; set; } 
} 

Включимо автоматичні міграції, щоб Entity Framework створив нам базу даних, і потім приводив би її структуру у відповідність з моделлю, коли вона зміниться. Для цього потрібно зайти в Tools -> NuGet Package Manager -> Package Manager Console і ввести команду:
Enable-Migrations-EnableAutomaticMigrations 


У нас в проекті з'явилася папка Migrations, а в ній клас Configuration.


Щоб застосувати цю конфігурацію до нашого контексту, можна створити для нього статичний конструктор
using BreezeJsDemo.Migrations; 
using System; 
using System.Collections.Generic; 
using System.Data.Entity; 
using System.Linq; 
using System.Web; 

namespace BreezeJsDemo.Model 
{ 
public class ShoppingListDbContext: DbContext 
{ 
static ShoppingListDbContext() 
{ 
Database.SetInitializer<ShoppingListDbContext>(new MigrateDatabaseToLatestVersion<ShoppingListDbContext, Configuration>()); 
} 

public DbSet<ListItem> ListItems { get; set; } 
public DbSet<Category> Categories { get; set; } 
} 
} 

Тепер при першому використанні контексту Entity Framework подбає про створення бази даний, якщо її немає, або про додавання відсутніх таблиць, стовпців, зв'язків, якщо потрібно. У нашій програмі немає жодного рядка підключення, тому Entity створить базу SQL Server Compact в папці App_Data, або, якщо у Вас на машині встановлений SQL Express, база буде створена на ньому.

Контролер Breeze
Далі створимо WebApi контролер, який буде віддавати і зберігати наші дані. Для цього створюємо папку Controllers (за угодою, або будь-яку іншу), натискаємо на неї правою кнопкою Add -> Controller і вибираємо Web API Controller — Empty, назвемо його, наприклад, DbController.
using Breeze.ContextProvider; 
using Breeze.ContextProvider.EF6; 
using Breeze.WebApi2; 
using BreezeJsDemo.Model; 
using Newtonsoft.Json.Linq; 
using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Net; 
using System.Net.Http; 
using System.Web.Http; 

namespace BreezeJsDemo.Controllers 
{ 
[BreezeController] 
public class DbController : ApiController 
{ 
private EFContextProvider<ShoppingListDbContext> _contextProvider = new EFContextProvider<ShoppingListDbContext>(); 

public String Metadata () { 
return _contextProvider.Metadata(); 
} 

[HttpGet] 
public IQueryable < ListItem> ListItems() 
{ 
return _contextProvider.Context.ListItems; 
} 

[HttpGet] 
public IQueryable<Category> Categories() 
{ 
return _contextProvider.Context.Categories; 
} 

[HttpPost] 
public SaveResult SaveChanges(JObject saveBundle) 
{ 
return _contextProvider.SaveChanges(saveBundle); 
} 
} 
} 

Розберемо цей код детальніше. Для роботи з Breeze його розробники радять створювати всього один контролер на базу даних, як вони кажуть:
«One controller to rule them all ...»
Це звичайний ApiController з атрибутом BreezeControllerAttribute, який виконує для нас ряд налаштувань.

На самому початку ми створили примірник EFContextProvider, у нас він виконує три завдання:
  • Створює екземпляр DbContext
  • Допомагає нам отримати метадані в необхідному для клієнта форматі
  • Обробляє запит на збереження даних
Метадані ми повертаємо методом Metadata, саме цим шляхом їх буде шукати клієнт. Всі сутності, з якими ми хочемо працювати повертаємо, як IQueryable<>, у відповідних методах, по одному на кожну. Можна обмежити операції з тією чи іншою сутністю за допомогою атрибута BreezeQueryableAttribute:
[BreezeQueryable(AllowedQueryOptions= AllowedQueryOptions.Skip | AllowedQueryOptions.Top)]

Він успадковується від QueryableAttribute, і користуватися і можна абсолютно аналогічно.

Також звернемо увагу на єдиний метод SaveChanges, він приймає, можливо, вже знайомий Вам JObject, містить усі поточні зміни, а EFContextProvider виконує валідацію і зберігає всі зміни в базу, використовуючи наш DbContext.

Клієнт
Перед роботою з javascript, html, css, less в Visual Studio 2013 дуже рекомендую (якщо Ви ще цього не зробили) встановити Update 4, і після оновлення зайти в Tools -> Extensions and Updates -> Online стане доступний Extension Web Essentials 2013 for Update 4, в тому числі і в Express версії. Update 4 і цей чудовий плагін, додає величезна кількість зручних інструментів для веб-розробника, дійсно must-have, детальніше про всі функції можна почитати на Офіційному сайті розробників. Visual Studio підтримує intellisense і javascript для цього всього лише потрібно в папку зі скриптами додати файл _references.js і додати в нього посилання на ваші .js файли, щоб студія змогла їх проіндексувати. Так ось Web Essentials може зробити це за Вас, він створить файл з параметром autosync, і потім студія буде підтримувати його в актуальному стані, для цього потрібно просто натиснути правою кнопкою на папці Scripts -> Add -> _references.js Intellisense file, на мій погляд, з автодополнением працювати набагато зручніше


Приступимо до створення клієнтської частини. Скрипти і розмітку додатки будемо зберігати в папці app. Створюємо файл з модулем програми app.module.js,
(function () {
'use strict';

angular.module('app', ['ngRoute', 'breeze.angular', 'angular-loading-bar']);
})();

Ми оголосили модуль нашого додатка, перший параметр — ім'я модуля, другий — перерахування залежностей — інших модулів, які використовуються в нашому. Тут ми вказали залежність від модуля breeze.angular, в ньому є сервіс breeze. В принципі, можна обійтися і без нього, і тоді звертатися до об'єкта window.breeze, він працює абсолютно аналогічно, за винятком одного моменту: сервіс breeze використовує $q $http angular.js і може ініціювати digest, і тому переважніше. angular-loading-bar — симпатичний індикатор завантаження, що працює з коробки без всяких налаштувань, треба тільки додати залежність, буде наочно показувати нам, коли breeze завантажує дані з сервера. ngRoute — це стандартний модуль angular.js, що відповідає за роутинг, створимо файл з налаштуваннями шляхів app.config.routes.js.
(function () {
'use strict';

angular.module('app').config(config);

config.$inject = ['$routeProvider']; 

function config($routeProvider) {
$routeProvider.
when('/', {
templateUrl: '/app/shoppingList/shoppingList.html'
}).
otherwise({
redirectTo: '/'
});
}
})();


Розберемо детально. Тут у зверненні до angular.module немає другого параметра, це означає, що ми не створюємо новий модуль, а беремо посилання на вже існуючий. Потім викликаємо у нього метод config, якому передаємо функцію конфігурації. Функції config додаємо властивість $inject, йому присвоїмо масив зі списком залежностей, які angular передасть функції у вхідні параметри. Тут ми запросили провайдер $routeProvider, він використовується для конфігурації шляхів застосування. Метод when задає відповідність між адресою та файлом розмітки на сервері. Тобто для нашого єдиного адреси "/" буде завантажена розмітка з файлу '/app/shoppingList/shoppingList.html' всередину тега з директивою ng-view. Метод otherwise дозволяє налаштувати поведінку, у разі, якщо адреса не збігається ні з одним з роутов, зазначених за допомогою when, в нашому випадку відбудеться редирект на адресу "/".

Для view з нашим списком покупок створимо в папці /app/shoppingList контролер shoppingList.controller.js.

(function () {
'use strict';

angular.module('app').controller('ShoppingListController', ShoppingListController);

ShoppingListController.$inject = ['$scope', 'clear'];

function ShoppingListController($scope, breeze) {
var vm = this;

vm.newItem = {};
vm.refreshData = refreshData;
vm.isItemExists = isItemExists;
vm.saveChanges = saveChanges;
vm.rejectChanges = rejectChanges;
vm.hasChanges = hasChanges;
vm.addNewItem = addNewItem;
vm.deleteItem = deleteItem;
vm.filterByCategory = filterByCategory;

breeze.NamingConvention.camelCase.setAsDefault();

var manager = new breeze.EntityManager("breeze/db");
var categoriesQuery = new breeze.EntityQuery("Categories").using(manager).expand("listItems");
var listItemsQuery = new breeze.EntityQuery("ListItems").using(manager);

activate();

function activate() {

refreshData();

$scope.$watch('vm.filterCategory', function (a, b) {
if (a !== b) {
refreshData();
}
});
}

function refreshData() {

var query = listItemsQuery;
if (vm.filterCategory) {
query = query.where('category.id', breeze.FilterQueryOp.Equals, vm.filterCategory.id);
}

categoriesQuery.execute()
.then(
function (data) {
vm.categories = data.results;
})
.then(
function () {
vm.listItems = query.executeLocally();
}
);
}

function filterByCategory(cat) {
if (vm.filterCategory && vm.filterCategory.name === cat.name) {
vm.filterCategory = undefined;
} else {
vm.filterCategory = cat;
}
}

function saveChanges() {
manager.saveChanges();
}

function rejectChanges() {
manager.rejectChanges();
}

function hasChanges() {
return manager.hasChanges();
}

function addNewItem() {
var category = vm.categories.filter(function (x) { return x.name === vm.newItem.category; });

if (category.length === 0) {
category = manager.createEntity('Category', { name: vm.newItem.category });
vm.categories.push(category);
} else {
category = category[0];
}

var item = manager.createEntity('ListItem', { name: vm.newItem.name category: category, isBought: false });
vm.listItems.push(item);

vm.newItem = {};
}

function deleteItem(item) {
item.entityAspect.setDeleted();
}

function isItemExists(x) {
return x.entityAspect.entityState.name !== 'Deleted' && x.entityAspect.entityState.name !== 'Detached';
}
}
})();

Тут ми вказали залежність від сервісу breeze, про який я згадував вище, з ним і будемо працювати. Насамперед зверну увагу на рядок breeze.NamingConvention.camelCase.setAsDefault() — так ми змушуємо breeze переробляти імена всіх властивостей об'єктів у camelCase, адже так працювати в JavaScript набагато звичніше. Далі створюємо об'єкт EntityManager — він і дозволяє нам запитувати, створювати, видаляти сутності, стежить за їх змінами і відправляє їх на сервер. У конструктор передаємо адресу нашого контролера DbController. Для адрес контролерів з атрибутом BreezeController за замовчуванням використовується префікс "/breeze/". Потім створюємо запити EntityQuery, конструктор передаємо ім'я методу контролера, який повертає потрібну сутність. Далі за допомогою методу using вказуємо запитом його EntityManager(замість цього при запиті можна було використовувати метод EntityManager'а executeQuery). Слідом ми використовували метод expand, він повідомляє серверу про те, що ми хочемо завантажити ще й всі ListItem кожної категорії, доступ до них можна буде отримати через навігаційне властивість listItems.

Функція refreshData використовує метод EntityQuery where, щоб додати критерій фільтрації запиту listItemsQuery, якщо обрана категорія vm.filterCategory. Ми отримаємо всі ListItem, у яких 'Category.Id' дорівнює vm.filterCategory.id(у нас його встановлює функція filterByCategory). Другим параметром where передається одне із значень breeze.FilterQueryOp — це перерахування, яка містить всі допустимі оператори фільтрації. Для більш складних умов фільтрації метод where приймає об'єкт класу Predicate, який може містити в собі кілька умов. Далі для завантаження даних використовується метод EntityQuery execute, який виконує запит до методу контролера і повертає promise. По завершенні запиту ми записуємо результат в властивість categories контролера, щоб потім відобразити в розмітці. З допомогою expand ми завантажили не тільки категорії але і всі елементи списку, відповідно немає потреби запитувати їх знову по мережі, тому слідом ми використовували метод executeLocally, щоб зробити запит даних з кеша, присвоюємо результат властивості vm.listItems.

EntityQuery містить ще багато корисних методів, крім where, наприклад:
orderBy/orderByDesc(n) — задає сортування по властивості n
select('a, b, c,… n') — возволяет вибирати не сутність, а проекцію, що містить тільки властивості a, b і c… n
take/top — вибирає перші n записів, дуже зручно для розбиття на сторінки
skip(n) — пропускає n записів, чудово в поєднанні з take
inlineCount — при використанні skip/take(top) повертає так само загальне число записів
executeLocally — робить запит в кеш, без використання мережі
noTracking — змушує breeze повертати результат у вигляді простих javascript об'єктів, а не сутностей (EntityManager не буде відстежувати їх зміни)
і ще кілька інших…

Як вже згадувалося вище, EntityManager відстежує всі зміни, видалення і додавання сутностей. Потім, всі зміни можна відправити на сервер для збереження у базі даних. Для цього використовується метод saveChanges, він викликається асинхронно і повертає promise. З'ясувати, чи відбувалися якісь зміни можна за допомогою методу hasChanges. Взагалі, кожна сутність має властивість _backingStore, де містяться дані, і entityAspect — містить властивості і методи, які представляють об'єкт, як сутність Breeze (статус об'єкта, валідації, оригінальні значення та інше). Властивості entityAspect.originalValues ми побачимо список вихідних значень усіх змінених властивостей. А властивість entityAspect.entityState містить поточний статус. У сутностей, в яких відбувалися зміни в entityAspect.entityState буде статус «Modified», а у незмінних буде статус «Unchanged». Так само є статуси: Deleted(об'єкт віддалений), Added(новий об'єкт) і Detached(об'єкт «відкріплений» від EntityManager, відстеження змін не відбувається, такий об'єкт потім можна «прикріпити» до будь-якого менеджера з допомогою методу attachEntity). EntityManager так само дозволяє скасувати всі зміни, які сталися за допомогою методу rejectChanges.

Тепер розглянемо функцію addNewItem, з допомогою якої ми додаємо новий елемент списку. Спочатку ми шукаємо по імені категорію нового елемента списку vm.categories, і, якщо такої категорії у нас ще немає — створюємо її за допомогою методу EntityManager — createEntity, першим параметром передаємо ім'я типу створюваної сутності(або об'єкт EntityType), другим — об'єкт, який містить значення властивостей створюваного об'єкта. Так само можна вказати ще два параметри: EntityState — задає статус створеного об'єкта і MergeStrategy — стратегію вирішення конфліктів, у випадках, коли сутність з таким ключем вже існує. Потім додаємо таким же чином новий ListItem.

При натисканні на кнопку видалення елемента списку буде викликатися функція deleteItem. У ній ми використовуємо метод сутності entityAspect.setDeleted(), він встановлює їй статус 'Deleted', і потім, при виклику saveChanges запис у базі буде видалена.

Так само у нас є функція isItemExists, вона буде використовуватися для фільтрації списку, щоб не показувати елементи, котрі вже видалені.

Перейдемо до розмітки, додамо в корінь проекту файл index.html він буде мати наступний вигляд:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Лист покупок</title>
<link href="Content/bootstrap.css" rel="stylesheet" />
<link href="Content/loading-bar.min.css" rel="stylesheet" />
</head>
<body ng-app="app">
<div ng-view></div>
<script src="Scripts/jquery-2.1.1.js"></script>
<script src="Scripts/bootstrap.js"></script>
<script src="Scripts/angular.js"></script>
<script src="Scripts/angular-route.js"></script>
<script src="Scripts/loading-bar.min.js"></script>
<script src="Scripts/breeze.debug.js"></script>
<script src="Scripts/breeze.angular.js"></script>

<script src="app/app.module.js"></script>
<script src="app/app.config.routes.js"></script>
<script src="app/shoppingList/shoppingList.controller.js"></script>
<!--На час розробки, звичайно, зручно, просто перетягувати файли скриптів в розмітку,
але на практиці, це дасть купу GET запитів до несжатым файлів,
тому звичайно краще покласти скрипти до єдиного стислий bundle, або використовувати бібліотеки типу RequireJS.-->
</body>
</html>


Тут варто звернути увагу на атрибут ng-app — він використовується, щоб вказати Angular кореневий елемент для нашого додатка, часто для цього використовують елементи html, body. Атрибут ng-view — вказує елемент, в який буде завантажена розмітка із файлу, що знаходиться по дорозі templateUrl поточного роута.

Для нашого єдиного маршруту '/' це буде файл '/app/shoppingList/shoppingList.html'
<div class="container" ng-controller="ShoppingListController as vm">
<nav class="navbar navbar-default">
<ul class="navbar-nav nav">
<li ng-if="vm.hasChanges()"><a ng-click="vm.saveChanges()"><span class="glyphicon glyphicon-thumbs up"></span> Зберегти зміни</a></li>
<li ng-if="vm.hasChanges()"><a ng-click="vm.rejectChanges()"><span class="glyphicon glyphicon-thumbs-down"></span> Скасувати зміни</a></li>
<li><a ng-click="vm.refreshData()"><span class="glyphicon glyphicon-refresh"></span> Оновити</a></li>
</ul>
</nav>
<h1>Список покупок</h1>
<div ng-if="vm.categories.length>0">
<h4>Фільтр за категоріями</h4>
<ul class="nav nav-pills">
<li ng-repeat="cat in vm.categories" ng-class="{active:vm.filterCategory===cat}" ng-if="cat.listItems.length>0">
<a ng-click="vm.filterByCategory(cat)">{{cat.name}} ({{cat.listItems.length}})</a>
</li>
</ul>
</div>
<table class="table table-striped">
<tbody>
<tr>
<td>&Додати lt;/td>
<td><input class="form-control" ng-model="vm.newItem.category" placeholder="Категорія" /></td>
<td><input class="form-control" ng-model="vm.newItem.name" placeholder="Назву" /></td>
<td><button class="btn btn-success btn-sm" type="button" ng-click="vm.addNewItem()"><span class="glyphicon glyphicon-plus"></span></button></td>
</tr>
<tr ng-repeat="item in vm.listItems | filter: vm.isItemExists | orderBy:'isBought'">
<td><input type="checkbox" ng-model="item.isBought"> Куплено</td>
<td>{{item.category.name}}</td>
<td>{{item.name}}</td>
<td><button class="btn btn-danger" type="button" ng-click="vm.deleteItem(item)"><span class="glyphicon glyphicon-trash"></span></button></td>
</tr>
</tbody>
</table>
</div>



Атрибут ng-controller «приєднує» контролер до елемента розмітки. Тут ми використовували синтаксис "controller as", тобто вказавши «ShoppingListController as vm» ми присвоїли контролеру псевдонім vm, і далі в розмітці можемо звертатися до властивостей контролера через крапку, наприклад vm.listItems(в коді контролера першої рядком написали var vm = this; — це було зроблено для зручності, щоб в коді конроллера звертатися до властивостей так само). У багатьох туториалах за Angular.js використовується дещо інший підхід, значення присвоюються властивостям об'єкта $scope, а в ng-controller пишеться тільки ім'я контролера, і потім в розмітці до цим властивостям можна звертатися просто по імені, наприклад: {{newItem.name}}, але одного разу приходить момент, коли потрібно використовувати контролер всередині іншого контороллера, і вони обидва мають властивості з однаковими іменами, тоді щоб звернутися до властивості батьківського контролера доводиться писати конструкції на кшталт "$parent.$parent.property", замість того, щоб звернутися до нього через псевдонім, тому має сенс взяти собі за правило використання синтаксису «controller as».

Далі йде верхнє меню з кнопками «Зберегти зміни», «Скасувати», «Оновити», перші дві ми ховаємо за допомогою ng-if, якщо змін немає, а за допомогою ng-click призначаємо кнопок виклик відповідних функцій.

Потім намалюємо список категорій з допомогою ng-repeat, директива ng-class="{active:vm.filterCategory===cat}" встановить елементу клас active, якщо виконується умова vm.filterCategory===cat, тобто подкрасит обрану категорію. Слідом відобразимо таблицю з нашим списком покупок, першим рядком будуть йти поля вводу назви і категорії з кнопкою Додати, а далі буде йти безпосередньо список ng-repeat=«item in vm.listItems | filter: vm.isItemExists | orderBy:'isBought'», тут ми використовували фільтр filter, якому вказали функцію vm.isItemExists, щоб відобразити тільки існуючі елементи, фільтр orderBy відсортує список значенням властивості isBought, щоб всі куплені переміщалися вниз.

Висновок
Мабуть, це і все, що хотілося розповісти на перший раз (навіть трохи більше). Статтю почав писати ще влітку, але із-за відсутності часу закінчив тільки зараз. І за ці пів року ми так і не перестали користуватися breeze.js. Особливо швидко і зручно реалізовувати на ньому програми, де потрібен грід з сортировками, пошуком та розбиттям на сторінки. Так само в два рахунки робиться в них і редагування, особливо якщо валідація не виходить за рамки атрибутів валідації на моделях EntityFramework.

З підводних каменів поки що помітили лише те, що breeze не підтримує зв'язок моделей many-to-many без проміжного класу(коли EntityFramework «додумує» проміжну таблицю за нас), з проміжним ж класом, природно, все добре.

Для тих, у кого немає часу створювати новий проект, але є бажання поекспериментувати з бризом — посилання на готовий solution.

P. S. Пишу вперше, тому прошу читача приділити хвилинку і висловити в коментарях свої зауваження щодо змісту/оформлення/стилю викладу і, якщо є, побажання для майбутніх статей.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.