Boa tarde
Eu usei o WPF por muitos anos. O padrão MVVM é provavelmente um dos padrões arquiteturais mais convenientes. Eu assumi que o MVC é quase o mesmo. Quando vi o uso do MVC na prática em um novo local de trabalho, fiquei surpreso com a complexidade e ao mesmo tempo a falta de usabilidade elementar. O mais irritante é que a validação ocorre apenas quando o formulário está sobrecarregado. Não há quadros vermelhos destacando o campo em que o erro, mas apenas um alerta com uma lista de erros. Se houver muitos erros, será necessário corrigir alguns erros e salvar para salvar para repetir a validação. O botão Salvar está sempre ativo. Listas vinculadas são realmente implementadas através de js, mas são complicadas e confusas. O modelo, a vista e o controlador estão fortemente acoplados, portanto, teste tudo magnificência muito dificil
Como lidar com isso? Para quem é interessante, pergunto sob kat.
E assim temos:
A construção de formulários MVC de forma clássica não implica outra maneira de interagir com o servidor, sobrecarregando a página inteira, o que não é conveniente para o usuário.
O uso completo de estruturas como Reart, Angular, Vue e a transição para SinglePageApplicatrion tornaria possível a criação de interfaces mais convenientes, mas, infelizmente, em princípio, não é possível na estrutura deste projeto, pois:
- Muitos códigos foram escritos, aceitos e ninguém permitirá que você os refaça.
-Nós somos programadores em C # e não conhecemos js na quantidade certa.
Além disso, as estruturas Reart, Angular, Vue são aprimoradas para escrever lógica complexa no cliente, o que, na minha aparência do WPF, não está correto. Toda a lógica deve estar em um local e este é um objeto de negócios e / ou classe de modelo. A visualização deve exibir apenas o estado do modelo.
Com base no exposto, tentei encontrar uma abordagem que permita obter a funcionalidade máxima com o mínimo de código js. Primeiro de tudo, o código mínimo que precisa ser gravado para gerar e atualizar um campo específico.
Meu pacote VueJs + MVC proposto é assim:
- O VueJs é usado na versão mais simples com conexão via cdn. Se necessário, os componentes podem ser conectados via CDN.
- Após o carregamento, o Vue carrega os dados do formulário através do Ajax.
- Sempre que o formulário é alterado, o Vue envia todas as alterações para o servidor (para campos de texto, você pode configurar que as alterações sejam enviadas quando o foco for perdido).
- A validação ocorre no servidor através do mecanismo de entidade e os campos inválidos são retornados ao cliente e um sinal de que o estado do modelo foi alterado em relação ao banco de dados.
-Se a próxima solicitação de validação ocorrer antes da anterior, a solicitação de validação anterior será cancelada.
O modelo MVC não é usado. A função ViewModel no sentido WPF é borrada aqui entre o vue e o controlador.
As vantagens dessa implementação sobre a página clássica do Razor: - A interface é desenhada usando as ferramentas Vue, projetadas para desenhar interfaces. A principal vantagem.
- Separando as camadas do ViewModel.
- erros de validação são exibidos à medida que o formulário é preenchido.
- teste de conveniência
Desvantagens: - Carga excessiva no servidor com solicitações de validação.
A necessidade de conhecer vue e js no mínimo.
Considero essa abordagem como o modelo inicial para trabalhar com o formulário.
Em um aplicativo real para um formulário específico, é desejável otimizar:
1) Envie uma solicitação de validação apenas ao alterar os campos, que devem ser validados no servidor.
2) A validação é longa, os campos estão cheios, etc. executado no cliente.
Então vamos lá.
No meu exemplo, usei o banco de dados de treinamento Northwind, que baixei com um dos exemplos do Devextreem.
A criação do aplicativo, a conexão da Entidade e a criação do DbContext I serão deixadas nos bastidores. Link para o github com um exemplo no final do artigo.
Crie um novo controlador vazio do MVC 5. Chame-o OrdersController. Até agora, existe um método.
public ActionResult Index() { return View(); }
Adicione mais um
public ActionResult Edit() { return View(); }
Agora você precisa ir para a pasta Views / Orders e adicionar duas páginas Index.cshtml e Edit.cshtml
Uma observação importante de que uma página cshtml funcionaria sem um modelo deve ser adicionada à parte superior da página herdada System.Web.Mvc.WebViewPage.
Supõe-se que Index.cshtml contenha uma tabela da qual uma linha destacada irá para a página de edição. Por enquanto, basta criar links que levarão à página de edição.
@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>
Agora eu quero implementar a edição de um objeto existente.
A primeira coisa a fazer é descrever um método no controlador que retornaria uma descrição do objeto para o cliente Json por identificador.
[HttpGet] public ActionResult GetById(int id) { var order = _db.Orders.Find(id);
Você pode verificar se tudo funciona digitando no navegador (o número da porta é naturalmente seu) http: // localhost: 63164 / Orders / GetById? Id = 10501
Você deve obter algo assim no navegador
{ "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" }
Bem e (ou) escrevendo um teste simples. No entanto, vamos deixar o teste além do escopo deste artigo.
[Test] public void OrderControllerGetByIdTest() { var bdContext = new Northwind(); var id = bdContext.Orders.First().OrderID;
Em seguida, você precisa criar um formulário do 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>
Se tudo for feito corretamente, o protótipo do formulário futuro deverá ser exibido no navegador.

Como podemos ver, o Vue exibiu todos os campos exatamente como o modelo. Mas os dados no modelo ainda são estáticos e a primeira coisa a fazer é implementar o carregamento de dados do banco de dados através do método acabado de escrever.
Para fazer isso, adicione o método fetchOrder () e chame-o na seção montada:
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: {
Bem, como o identificador do objeto agora deve vir do controlador, no controlador, você precisa passar o identificador para o objeto dinâmico do ViewBag para que ele possa ser obtido no modo de exibição.
public ActionResult SimpleEdit(int id = 0) { ViewBag.Id = id; return View(); }
Isso é suficiente para ler os dados no momento da inicialização.
É hora de personalizar o formulário.
Para não sobrecarregar o artigo, deduzi um mínimo de campos. Sugiro que os iniciantes descubram como trabalhar com listas vinculadas.
<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>
Os campos ShipCountry e ShipAddress são os melhores candidatos para listas vinculadas.
Aqui estão os métodos do controlador. Como você pode ver, tudo é bem simples, toda a filtragem é feita usando o Linq.
Mas no código View foi adicionado significativamente mais.
Além das funções dos países e cidades, é necessário adicionar um relógio que monitora as alterações do objeto; infelizmente, o valor antigo do objeto vue complexo não salva, portanto, você deve salvá-lo manualmente, para o qual criei o método saveOldOrderValue: enquanto salvo apenas o país nele. Isso permite reler a lista de cidades somente quando o país muda. Caso contrário, o código é o mesmo, eu acho. No exemplo, mostrei apenas uma lista vinculada de nível único (por esse princípio não é difícil fazer o aninhamento de nenhum nível).
@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: { </script> </body> </html>
Um tópico separado é Validação. Do ponto de vista da otimização de velocidade, é claro, você precisa fazer a validação no cliente. Mas isso levará à duplicação de código, por isso estou mostrando um exemplo com validação no nível da Entidade (como deveria ser ideal). Ao mesmo tempo, o código mínimo, a validação em si ocorre muito rapidamente e também de forma assíncrona. Como a prática demonstrou, mesmo com uma Internet muito lenta, tudo funciona mais do que o normal.
Os problemas só surgem se o texto for digitado rapidamente em um campo de texto e a velocidade de digitação for de 260 caracteres por minuto. A opção de otimização mais simples para os campos de texto é definir o modelo v de atualização lenta .lazy = "order.ShipAddress"; a validação ocorrerá quando o foco for alterado. Uma opção mais avançada é atrasar a validação desses campos + se a próxima solicitação de validação for chamada antes de receber uma resposta e ignorar o processamento da solicitação anterior.
Os métodos para processar a validação no controle foram os seguintes.
[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;
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; } } }
Bastante detalhado, mas esse código foi escrito uma vez para todo o aplicativo e não requer ajuste para um objeto ou campo específico. Como resultado do método que trabalha no cliente, o objeto json com as propriedades IsChanded e Errors. Naturalmente, essas propriedades precisam ser criadas em nosso Vue e preenchidas a cada alteração do objeto.
Para obter erros de validação, você precisa definir essa validação em algum lugar. É hora de adicionar alguns atributos de validação à nossa descrição do objeto Entidade do pedido.
[MinLength(10)] [StringLength(60)] public string ShipAddress { get; set; } [CheckCityAttribute(" ShipCity ")] public string ShipCity { get; set; }
MinLength e StringLength são atributos padrão, mas para ShipCity eu criei um atributo personalizado
No entanto, vamos deixar o tópico Validação de entidade também fora do escopo deste artigo.
Para exibir erros, você precisa adicionar um link ao Css e modificar ligeiramente o formulário.
É assim que nosso formulário modificado deve ficar agora:
@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: { </script> </body> </html>
Parece 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 }
E aqui está o resultado do trabalho.

Para entender como a validação funciona, vamos examinar com atenção a marcação que descreve um campo:
<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>
Aqui estão dois pontos importantes:
Esta parte da marcação conecta o estilo responsável pelo quadro vermelho ao redor do elemento v-bind: class = "{error :! Errors.Freight == ''} aqui o vue conecta a classe css por condição.
E aqui está esta janela pop-up mostrada quando o cursor do mouse está sobre um elemento:
<span v-if="errors.Freight!==''" class="tooltiptext">{{errors.Freight}}</span>
além disso, o elemento pai do elemento deve conter o atributo class = "tooltip".
Na última versão, o botão salvar é adicionado configurado para que estivesse disponível apenas se fosse possível salvar.
Para simplificar a marcação necessária para a validação, proponho escrever o componente mais simples que levaria toda a validação em si.
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>' });
agora a marcação parece mais limpa.
<error-aborder v-bind:error="errors.Freight"> <input type="number" v-model="order.Freight" class="input" /> </error-aborder>
O desenvolvimento se resume a organizar os campos em um formulário, configurar a validação no Entyty e criar listas. Se as listas são estáticas e não são grandes, elas podem ser completamente definidas no código.
A parte C # do código é bem testada. Os próximos planos lidam com o teste Vue.
Era tudo o que eu queria contar.
Eu apreciaria muito as críticas construtivas.
Aqui está o link para o código fonte .
No exemplo, o formulário é chamado SimpleEdit e contém a versão mais recente. Qualquer pessoa interessada em opções preliminares pode passar por confirmações.
No exemplo, implementei a otimização: abortando a solicitação de validação se, sem aguardar a resposta da validação, causar a validação pela segunda vez.