Pull to refresh

VueJs + MVC минимум кода максимум функциональности

Reading time16 min
Views16K

Добрый день.


Я много лет использовал WPF. Паттерн MVVM наверное один из наиболее удобных архитектурных паттернов. Я предполагал что MVC почти то же самое. Когда я на новом месте работы я увидел использование MVC на практике, то был удивлен запутанностью и одновременно отсутствием элементарной Юзабилити. Больше всего раздражает то, что валидация происходит только при перегрузке формы. Нет красных рамок подсвечивающих поле в котором ошибка, а просто выводится alert со списком ошибок. Если ошибок много, то приходится исправлять часть ошибок и жать сохранить, что бы повторить валидацию. Кнопка сохранить всегда активна. Связанные списки правда реализованы через js, но сложно и запутанно. Модель, представление и контроллер сильно связаны поэтому протестировать все это великолепие весьма сложно.
Как с этим бороться ?? Кому интересно прошу под кат.


И так что мы имеем:
Построение форм MVC в классическом виде не предполагает другого способа взаимодействия с сервером как перегрузка страницы целиком, что не удобно пользователю.
Полноценное использование фреймворков типа Reart,Angular,Vue и переход на SinglePageApplicatrion позволило бы делать более удобные интерфейсы, но к сожалению в принципе не возможно в рамках данного проекта так как:
-Много кода написано, принято и ни кто не даст переделывать.
-Мы в программисты С# и не знаем js в нужном объеме.


Кроме этого фреймворки Reart, Angular, Vue заточены под написание сложной логики на клиенте, что на мой WPF-ный взгляд не правильно. Вся логика должна быть в одном месте и это бизнес объект и(или) класс модели. View должно всего лишь отображать состояние модели не более того.
Исходя из вышесказанного я постарался найти подход позволяющий с минимум кода на js получить максимум функциональности. В первую очередь минимуме кода который нужно писать для вывода и обновления конкретного поля.
Предлагаемая мной связка VueJs + MVC выглядит так:


  • VueJs используется в простейшем варианте с подключением через cdn. Компоненты если потребуются то же можно через cdn подключать.
  • После загрузки Vue загружает данные формы через Ajax.
  • При каждом изменении формы Vue отправляет на сервер все изменения (для текстовых полей можно настроить, что изменения посылаются при потере фокуса).
  • На сервере через механизм Entity происходит валидация и на клиент возвращаются невалидные поля и признак что состояние модели изменилось по отношению к базе данных.
    -Если очередной запрос валидации произойдет раньше, чем вернулся предыдущий, то предыдущий запрос валидации будет отменен.
    MVC модель не используется. Функция ViewModel в WPF-ном понимании здесь размазана между vue и контроллером.
    Преимущества такой реализации перед классической Razor страницей:
  • интерфейс рисуется средствами Vue который заточен под рисование интерфейсов. Главное преимущество.
  • разделение слоев View от ViewModel.
  • ошибки валидации отображаются по мере заполнения формы.
  • удобство тестирования
    Недостатки:
  • Излишняя нагрузка на сервер запросами валидации.
  • Необходимость знать vue и js в минимальном объеме.


    Я рассматриваю данный подход как начальный шаблон для работы с формой.
    В реальном приложении для конкретной формы желательно провести оптимизацию:
    1) Посылать запрос валидации только при изменении полей валидацию которых необходимо провести на сервере.
    2) Валидацию длинны, заполненность полей и т.п. выполнять на клиенте.



Итак поехали.


В качестве базы данных в своем примере я использовал учебную базу данных Northwind которую скачал с одним из примеров Devextreem.
Создание приложения, подключение Entity и создание DbContext я оставлю за кадром. Ссылка на github с примером в конце статьи.
Создаем новый пустой контроллер MVC 5. Назовем его OrdersController. В нем пока один метод.


   public ActionResult Index()
        {
            return View();
        }

Добавим еще один


       public ActionResult Edit()
        {
            return View();
        }

Теперь надо перейти в папку Views/Orders и добавить две страницы Index.cshtml и Edit.cshtml
Важное замечание, что бы cshtml страница работала без модели надо обязательно добавить в начало страницы inherits System.Web.Mvc.WebViewPage.
Предполагается, что Index.cshtml содержит таблицу из которой по выделенной строке будет осуществляться переход на страницу редактирования. Пока создадим просто ссылки которые будут вести на страницу редактирования.


@inherits System.Web.Mvc.WebViewPage
<table >
    @foreach (var item in ViewBag.Orders)
    {
        <tr><td><a href="Edit?id=@item.OrderID">@item.OrderID</a></td></tr>
    }
</table>

Теперь я хочу реализовать редактирование существующего объекта.


Первое, что необходимо сделать, это описать метод в контроллере который бы по идентификатору возвращал бы на клиент Json описание объекта.


        [HttpGet]
        public ActionResult GetById(int id)
        {
            var order = _db.Orders.Find(id);//Получили объект
            string orderStr = JsonConvert.SerializeObject(order);//Сериализовали его
            return Content(orderStr, "application/json");//отправили 
        }

Проверить, что все работает можно набрав в браузере (номер порта естественно ваш) http://localhost:63164/Orders/GetById?id=10501
Вы должны получить в браузере что то вроде


{
  "OrderID": 10501,
  "CustomerID": "BLAUS",
  "EmployeeID": 9,
  "OrderDate": "1997-04-09T00:00:00",
  "RequiredDate": "1997-05-07T00:00:00",
  "ShippedDate": "1997-04-16T00:00:00",
  "ShipVia": 3,
  "Freight": 8.85,
  "ShipName": "Blauer See Delikatessen",
  "ShipAddress": "Forsterstr. 57",
  "ShipCity": "Mannheim",
  "ShipRegion": null,
  "ShipPostalCode": "68306",
  "ShipCountry": "Germany"
}

Ну и (или) написав простейший тест. Однако оставим тестирование за рамками данной статьи


       [Test]
        public void OrderControllerGetByIdTest()
        {
            var bdContext = new Northwind();
            var id = bdContext.Orders.First().OrderID; //получил первый существующий идентификатор

            var orderController = new OrdersController();
            var json = orderController.GetById(id) as ContentResult;

            var res = JsonConvert.DeserializeObject(json.Content,typeof(Order)) as Order;
            Assert.AreEqual(id, res.OrderID);
        }

Далее необходимо создать Vue форму.


@inherits System.Web.Mvc.WebViewPage
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>редактирование </title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
    <div id="app">
                        <h1>Aвто генерация формы</h1>
                        <table >
                            <tr v-for="(item,i) in order"> @*создание ряда по каждому свойству объекта ордер*@
                                <td> {{i}}</td>
                                <td>
                                    <input type="text" v-model="order[i]"/>
                                </td>
                            </tr>
                        </table>
    </div>

    <script>

    new Vue({
        el: "#app",
        data: {
            order: { 
                OrderID: 10501,
                CustomerID: "BLAUS",
                EmployeeID: 9,
                OrderDate: "1997-04-09T00:00:00",
                RequiredDate: "1997-05-07T00:00:00",
                ShippedDate: "1997-04-16T00:00:00",
                ShipVia: 3,
                Freight: 8.85,
                ShipName: "Blauer See Delikatessen",
                ShipAddress: "Forsterstr. 57",
                ShipCity: "Mannheim",
                ShipRegion: null,
                ShipPostalCode: "68306",
                ShipCountry: "Germany"
            }
        }
    });
    </script>
</body>
</html>

Если все сделано правильно, то в браузере должен отобразиться прототип будущей формы.



Как мы видим Vue отобразил все поля ровно так, как было модели. Но данные в модели пока статические и первое что нужно сделать дальше, это реализовать загрузку данных из базы через только что написанный метод.
Для этого добавим метод fetchOrder() и будем вызывать его в секции mounted:


        new Vue({
            el: "#app",
            data: {
                id: @ViewBag.Id,
                order: {
                    OrderID: 0,
                    CustomerID: "",
                    EmployeeID: 0,
                    OrderDate: "",
                    RequiredDate: "",
                    ShippedDate: "",
                    ShipVia: 0,
                    Freight: 0,
                    ShipName: "0",
                    ShipAddress: "",
                    ShipCity: "",
                    ShipRegion: null,
                    ShipPostalCode: "",
                    ShipCountry: ""
                },
            },
            methods: {
                //читаем объект
                fetchOrder() {
                    var path = "../Orders/GetById?key=" + this.id;
                    console.log(path);
                    this.fetchJson(path, json => this.order = json);
                },
                //обертка над стандартной функцией fetch
                fetchJson(path, collback) {
                    try {
                        fetch(path, { mode: 'cors' })
                            .then(response => response.json())
                            .then(function(json) { collback(json); }
                            );
                    } catch (ex) {
                        alert(ex);
                    }
                }
            },
            mounted: function() {
                this.fetchOrder();
            }
        });

Ну и так, как идентификатор объекта теперь должен приходить из контроллера, то в контроллере надо передaть идентификатор в динамический объект ViewBag, что бы его можно было получить во View.


        public ActionResult SimpleEdit(int id = 0)
        {
            ViewBag.Id = id;
            return View();
        }

Этого достаточно что бы данные начитывались при загрузке.
Настало время кастомизировать форму.
Что бы не перегружать статью я вывел минимум полей. Предлагаю для началa разобраться как работать с связанными списками.


  <table >
            <tr>
                <td>Стоимость перевозки</td>
                <td >
                    <input type="number" v-model="order.Freight" />
                </td>
            </tr>
            <tr>
                <td>Старана приписки корабля</td>
                <td>
                    <input type="text" v-model="order.ShipCountry"  />
                </td>
            </tr>
            <tr>
                <td>Город корабля</td>
                <td>
                    <input type="text" v-model="order.ShipCity" />
                </td>
            </tr>
            <tr>
                <td>Адрес корабля</td>
                <td>
                    <input type="text" v-model="order.ShipAddress" />
                </td>
            </tr>
        </table>

Поля ShipCountry и ShipAddress лучшие кандидаты на связанные списки.
Вот методы контроллера. Как видите все довольно просто.Вся фильтрация осуществляется с помощью Linq.


       /// <summary>
        /// Список доступных городов c учетом региона и страны
        /// если регион или страна не заданы , то все города 
        /// </summary>
        /// <param name="country"></param>
        /// <param name="region"></param>
        /// <returns></returns>
        [HttpGet]
        public ActionResult AvaiableCityList( string country,string region=null)
        {
            var avaiableCity =  _db.Orders.Where(c => ((c.ShipRegion == region) || region == null)&& (c.ShipCountry == country) || country == null).Select(a => a.ShipCity).Distinct();

            var jsonStr = JsonConvert.SerializeObject(avaiableCity);
            return Content(jsonStr, "application/json");
        }

        /// <summary>
        /// Список доступных стран c учетом региона
        /// если регион не задан, то все страны
        /// </summary>
        /// <param name="region"></param>
        /// <returns></returns>
        [HttpGet]
        public ActionResult AvaiableCountrys(string region=null)
        {
            var resList = _db.Orders.Where(c => (c.ShipRegion == region)||region==null).Select(c => c.ShipCountry).Distinct();
            var json = JsonConvert.SerializeObject(resList);
            return Content(json, "application/json");
        }

А вот во View кода прибавилось значительно больше.
Кроме собственно функций начитки стран и городов приходится добавить watch который следит за изменениями объекта, к сожалению старое значение сложного объекта vue не сохраняет поэтому нужно сохранять его в ручную, для чего я придумал метод saveOldOrderValue: пока я сохраняю в нем только страну. Это позволяет перечитывать список городов только при изменении страны. В остальном код то же думаю понятен. В примере я показал только одноуровневый связанный список ( по этому принципу не сложно сделать вложенность любого уровня).


@inherits System.Web.Mvc.WebViewPage
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>редактирование </title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
    <div id="app">
        <table>
            <tr>
                <td>Cтоимость перевозки</td>
                <td>
                    <input type="number" v-model="order.Freight" />
                </td>
            </tr>
            <tr>
                <td>Старана приписки корабля</td>
                <td>
                    <select v-model="order.ShipCountry" class="input">
                        <option v-for="item in AvaialbeCountrys" :v-key="item">{{item}} </option>
                    </select>
                </td>
            </tr>
            <tr>
                <td>Город корабля</td>
                <td>
                    <select v-model="order.ShipCity" >
                        <option v-for="city in AvaialbeCitys" :v-key="city">{{city}} </option>
                    </select>
                </td>
            </tr>
            <tr>
                <td>Адрес корабля</td>
                <td>
                    <input type="text" v-model="order.ShipAddress" />
                </td>
            </tr>
        </table>

    </div>

    <script>
        new Vue({
            el: "#app",
            data: {
                id: @ViewBag.Id,
                order: {
                    OrderID: 0,
                    CustomerID: "",
                    EmployeeID: 0,
                    OrderDate: "",
                    RequiredDate: "",
                    ShippedDate: "",
                    ShipVia: 0,
                    Freight: 0,
                    ShipName: "0",
                    ShipAddress: "",
                    ShipCity: "",
                    ShipRegion: null,
                    ShipPostalCode: "",
                    ShipCountry: ""
            },
            oldOrder: {
                ShipCountry: ""
            },
            AvaialbeCitys: [],
            AvaialbeCountrys: []
            },
            methods: {
                //читаем объект
                fetchOrder() {
                    var path = "../Orders/GetById?Id=" + this.id;
                    this.fetchJson(path, json => this.order = json);
                },
                fetchCityList() {
                    //город зависит от выбраной страны
                        var country = this.order.ShipCountry;
                        if (country == null || country === "") {
                            country = '';
                        }
                    var path = "../Orders/AvaiableCityList?country=" + country;
                    this.fetchJson(path, json => {this.AvaialbeCitys = json;});
                },
                fetchCountrys() {
                        var path = "../Orders/AvaiableCountrys";
                        this.fetchJson(path,jsonResult => {this.AvaialbeCountrys = jsonResult;});
                },
                //обертка над стандартной функцией fetch
                fetchJson(path, collback) {
                    try {
                        fetch(path, { mode: 'cors' })
                            .then(response => response.json())
                            .then(function(json) { collback(json); }
                            );
                    } catch (ex) {
                        alert(ex);
                    }
                },
                saveOldOrderValue:function(){
                  this.oldOrder.ShipCountry = this.order.ShipCountry;
                }
            },
            watch: {
                order: {
                    handler: function (after) {
                        if (this.oldOrder.ShipCountry !== after.ShipCountry)//Только если изменилась страна
                        {
                            this.fetchCityList();//Перечитываю список городов с учетом выбранной страны
                        }
                       this.saveOldOrderValue();
                     },
                    deep: true
                }
            },
            mounted: function () {
            this.fetchCountrys();//начитываю список стран
            //начитывать список городов здесь излишне, он начитается когда начитается объект
            this.fetchOrder();//читаю объект
            this.saveOldOrderValue();//запоминаю старое значение
            }
        });
    </script>
</body>
</html>

Отдельная тема Валидация. С точки зрения оптимизации скорости выполнения конечно надо сделать валидацию на клиенте. Но это приведет к дублированию кода, поэтому я показываю пример с валидацией на уровне Entity (Как собственно и должно быть в идеале). Кода при этом минимум, сама валидация происходит достаточно быстро и к тому же асинхронно. Как показала практика даже при весьма медленном интернете все работает более чем нормально.
Проблемы возникают только, если быстро набирается текст в текстовом поле, а скорость набора текста этак символов 260 в минуту. Простейший вариант оптимизации для текстовых полей установить ленивое обновление v-model.lazy="order.ShipAddress", тогда валидация произойдет при смене фокуса. Более продвинутый вариант сделать для этих полей задержку валидации + если следующий запрос валидации вызван раньше получения ответа, то обработку предыдущего запроса игнорировать.
Методы обработки валидации в контроле у меня получились вот такие.


      [HttpGet]
        public ActionResult Validate(int id, string json)
        {
            var order = _db.Orders.Find(id);
            JsonConvert.PopulateObject(json, order);
            var errorsD = GetErrorsJsArrey();
            return Content(errorsD.ToString(), "application/json");
        }

        private String  GetErrorsAndChanged()
        {
            var changed=  _db.ChangeTracker.HasChanges();
            var errors = _db.GetValidationErrors();
            return GetErrorsAndChanged(errors,changed);
        }

        private static string   GetErrorsAndChanged(IEnumerable<DbEntityValidationResult> errors,bool changed)
        {
            dynamic dynamic = new ExpandoObject();
            dynamic.IsChanged = changed;//Создание свойства IsChanged
            var errProperty = new Dictionary<string, object>();//Создание массива с будущими свойствами ошибки
            dynamic.Errors = new DynObject(errProperty);//Создание объекта у которого свойства задаются в массиве
            foreach (DbEntityValidationResult validationError in errors)//Заполнение массива ошибками
            {
                foreach (DbValidationError err in validationError.ValidationErrors)//Заполнение массива ошибками
                {
                    errProperty.Add(err.PropertyName,err.ErrorMessage);
                }
            }
            var json = JsonConvert.SerializeObject(dynamic); return json;
        }

И еще использую класс DynObject

 public sealed class DynObject : DynamicObject
    {
        private readonly Dictionary<string, object> _properties;

        public DynObject(Dictionary<string, object> properties)
        {
            _properties = properties;
        }

        public override IEnumerable<string> GetDynamicMemberNames()
        {
            return _properties.Keys;
        }

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            if (_properties.ContainsKey(binder.Name))
            {
                result = _properties[binder.Name];
                return true;
            }
            else
            {
                result = null;
                return false;
            }
        }

        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            if (_properties.ContainsKey(binder.Name))
            {
                _properties[binder.Name] = value;
                return true;
            }
            else
            {
                return false;
            }
        }
    }

Довольно многословно, но данный код пишется один раз на все приложение и не требует донастройки под конкретный объект или поле. В результате работы метода на клиент json объект со свойствами IsChanded и Errors. Эти свойства естественно нужно создать в нашем Vue и заполнять их при каждом изменении объекта.
Что бы получить ошибки валидации нужно эту валидацию где то задать. Самое время сейчас в нашем описании Entity объекта Order добавить несколько атрибутов валидации.


        [MinLength(10)]
        [StringLength(60)]
        public string ShipAddress { get; set; }

        [CheckCityAttribute("Поле ShipCity обязательно для заполнения")]
        public string ShipCity { get; set; }

MinLength и StringLength стандартные атрибуты, а вот для ShipCity я создал кастомный атрибут


   /// <summary>
    /// Custom Attribute Example
    /// </summary>
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public  class CheckCityAttribute : ValidationAttribute
    {
        public CheckCityAttribute(string message)
        {
            this.ErrorMessage = message;
        }
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            ValidationResult result = ValidationResult.Success;
            string[] memberNames = new string[] { validationContext.MemberName };
            string val = value?.ToString();
            Northwind _db = new Northwind();
            Order order = (Order)validationContext.ObjectInstance;
           bool exsist  =  _db.Orders.FirstOrDefault(o => o.ShipCity == val && o.ShipCountry == order.ShipCountry)!=null;

            if (!exsist)
            {
               result = new ValidationResult(string.Format(this.ErrorMessage,order.ShipCity , val), memberNames);
            }
            return result;
        }
    }

Однако давайте оставим тему валидации Entity тоже за рамками этой статьи.
Для того что бы отображать ошибки нужно добавить ссылку на Css и слегка доработать форму.
Вот так должна теперь выглядеть наша доработанная форма:


@inherits System.Web.Mvc.WebViewPage
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>редактирование id=@ViewBag.Id</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <link rel="stylesheet" type="text/css" href="~/Content/vueError.css" />
</head>
<body>
    <div id="app">
        <table>
            <tr>
                <td>Стоимость перевозки</td>
                <td class="tooltip">
                    <input type="number" v-model="order.Freight" v-bind:class="{error:!errors.Freight==''} " class="input" />
                    <span v-if="errors.Freight!==''" class="tooltiptext">{{errors.Freight}}</span>
                </td>
            </tr>
            <tr>
                <td>Страна приписки корабля</td>
                <td>
                    <select v-model="order.ShipCountry" class="input">
                        <option v-for="item in AvaialbeCountrys" :v-key="item">{{item}} </option>
                    </select>
                </td>
            </tr>
            <tr>
                <td>Город корабля</td>
                <td class="tooltip">
                    <select v-model="order.ShipCity" v-bind:class="{error:!errors.ShipCity==''}" class="input">
                        <option v-for="city in AvaialbeCitys" :v-key="city">{{city}} </option>
                    </select>
                    <span v-if="!errors.ShipCity==''" class="tooltiptext">{{errors.ShipCity}}</span>
                </td>
            </tr>
            <tr>
                <td>Адрес корабля</td>
                <td class="tooltip">
                    <input type="text" v-model.lazy="order.ShipAddress" v-bind:class="{error:!errors.ShipAddress=='' }" class="input" />
                    <span v-if="!errors.ShipAddress==''" class="tooltiptext">{{errors.ShipAddress}}</span>
                </td>
            </tr>
            <tr>
                <td> </td>
                <td>
                    <button v-on:click="Save()" :disabled="IsChanged===false" || hasError class="alignRight">Save</button>
                </td>
            </tr>
        </table>
    </div>
    <script>

        new Vue({
            el: "#app",
            data: {
                id: @ViewBag.Id,
                order: {
                    OrderID: 0,
                    CustomerID: "",
                    EmployeeID: 0,
                    OrderDate: "",
                    RequiredDate: "",
                    ShippedDate: "",
                    ShipVia: 0,
                    Freight: 0,
                    ShipName: "0",
                    ShipAddress: "",
                    ShipCity: "",
                    ShipRegion: null,
                    ShipPostalCode: "",
                    ShipCountry: ""
            },
            oldOrder: {
                ShipCountry: ""
            },
errors: {
                OrderID: null,
                CustomerID: null,
                EmployeeID: null,
                OrderDate: null,
                RequiredDate: null,
                ShippedDate: null,
                ShipVia: null,
                Freight: null,
                ShipName: null,
                ShipAddress: null,
                ShipCity: null,
                ShipRegion: null,
                ShipPostalCode: null,
                ShipCountry: null
  },
            IsChanged: false,
            AvaialbeCitys: [],
            AvaialbeCountrys: []
            },
            computed :
            {
                hasError: function () {
                    for (var err in  this.errors) {
                        var error = this.errors[err];
                        if (error !== '' || null) return true;
                    }
                    return false;
                }
            },
            methods: {
                //читаем объект
                fetchOrder() {
                    var path = "../Orders/GetById?Id=" + this.id;
                    this.fetchJson(path, json => this.order = json);
                },
                fetchCityList() {
                    //город зависит от выбранной страны
                        var country = this.order.ShipCountry;
                        if (country == null || country === "") {
                            country = '';
                        }
                    var path = "../Orders/AvaiableCityList?country=" + country;
                    this.fetchJson(path, json => {this.AvaialbeCitys = json;});
                },
                fetchCountrys() {
                        var path = "../Orders/AvaiableCountrys";
                        this.fetchJson(path,jsonResult => {this.AvaialbeCountrys = jsonResult;});
                },
                //обертка над стандартной функцией fetch
            Validate() {this.Action("Validate");},
            Save() {this.Action("Save");},
            Action(action) {
                var myJSON = JSON.stringify(this.order);
                var path = "../Orders/" + action + "?id=" + this.id + "&json=" + myJSON;
                this.fetchJson(path, jsonResult => {
                    this.errors = jsonResult.Errors;
                    this.IsChanged = jsonResult.IsChanged;
                });
            },
                fetchJson(path, collback) {
                    try {
                        fetch(path, { mode: 'cors' })
                            .then(response => response.json())
                            .then(function(json) { collback(json); }
                            );
                    } catch (ex) {
                        alert(ex);
                    }
                },
                saveOldOrderValue:function(){
                  this.oldOrder.ShipCountry = this.order.ShipCountry;
                }
            },
            watch: {
                order: {
                    handler: function (after) {
                     this.IsChanged=true;
                        if (this.oldOrder.ShipCountry !== after.ShipCountry)//Только если изменилась страна
                        {
                            this.fetchCityList();//Перечитываю список городов с учетом выбранной страны
                        }
                       this.saveOldOrderValue();
                   this.Validate();
                     },
                    deep: true
                }
            },
            mounted: function () {
            this.fetchCountrys();//начитываю список стран
            //начитывать список городов здесь излишне, он начитается когда начитается объект
            this.fetchOrder();//читаю объект
            this.saveOldOrderValue();//запоминаю старое значение
            }
        });
    </script>
</body>
</html>

Tак выглядит CSS


.tooltip {
    position: relative;
    display: inline-block;
    border-bottom: 1px dotted black;
}

.tooltip .tooltiptext {
    visibility: hidden;
    width: 120px;
    background-color: #555;
    color: #fff;
    text-align: center;
    border-radius: 6px;
    padding: 5px 0;
    position: absolute;
    z-index: 1;
    bottom: 125%;
    left: 50%;
    margin-left: -60px;
    opacity: 0;
    transition: opacity 0.3s;
}

.tooltip .tooltiptext::after {
    content: "";
    position: absolute;
    top: 100%;
    left: 50%;
    margin-left: -5px;
    border-width: 5px;
    border-style: solid;
    border-color: #555 transparent transparent transparent;
}

.tooltip:hover .tooltiptext {
    visibility: visible;
    opacity: 1;
}
.error  {
    color: red;
    border-color: red;
    border-style: double;
}
.input {

    width: 200px ;
}
.alignRight {
    float: right
}

А вот так результат работы.



Что бы разобраться как работает валидация давайте внимательно посмотрим на разметку описывающую одно поле:


<td class="tooltip">
                    <input type="number" **v-model="order.Freight" v-bind:class="{error:!errors.Freight==''} " **class="input" />
                    <span v-if="errors.Freight!==''" class="tooltiptext">{{errors.Freight}}</span>
                </td>

Здесь 2 важных ключевых момента:


Эта часть разметки подключает стиль ответственный за красную рамку вокруг элемента v-bind:class="{error:!errors.Freight==''} тут vue подключает по условию css класс .


А вот эта за всплывающее окно показываемое когда курсор мыши над над элементом:


  <span v-if="errors.Freight!==''" class="tooltiptext">{{errors.Freight}}</span>

кроме этого элемент родительский элемент должен содержать атрибут class="tooltip".


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


Vue.component('error-aborder',
    {
        props: {
            error: String
        },
        template:
            '<div class="tooltip" >' +
                '<div v-bind:class="{error:!error==\'\' }" >' +
                '<slot>test</slot>' +
                '</div>' +
                '<p  class="tooltiptext"  v-if="!error==\'\'" >{{error}}</p>' +
                '</div>'
    });

теперь разметка выглядит более аккуратно.


 <error-aborder v-bind:error="errors.Freight">
                        <input type="number" v-model="order.Freight" class="input" />
</error-aborder>

Разработка сводится к расположению полей на форме, настройке валидации в Entyty и формированию списков. Если списки статичные и не большие, то их вполне можно задавать в коде.


C# часть кода отлично тестируется. В ближайших планах разобраться с тестированием Vue.


Вот собственно и все что я хотел рассказать.
Буду очень признателен за конструктивную критику.


Вот ссылка на исходный код.


В примере форма называется SimpleEdit и содержит последнюю версию. Кому интересны предварительные варианты можно пройти по комитам.
В примере реализовал оптимизацию: прерывание запроса валидации если не дожидаясь ответа валидации вызвать валидацию второй раз.

Tags:
Hubs:
+6
Comments14

Articles

Change theme settings