مساء الخير
لقد استخدمت WPF لسنوات عديدة. نمط MVVM هو على الأرجح أحد الأنماط المعمارية الأكثر ملاءمة. افترضت أن MVC هو نفسه تقريبا. عندما رأيت استخدام MVC في الممارسة العملية في مكان عمل جديد ، فوجئت بالتعقيد وفي الوقت نفسه الافتقار إلى قابلية الاستخدام الأولية. الأكثر مزعج هو أن التحقق من الصحة يحدث فقط عندما يتم تحميل النموذج بشكل زائد. لا توجد إطارات حمراء تبرز الحقل الذي يوجد فيه الخطأ ، ولكن مجرد تنبيه مع قائمة بالأخطاء. إذا كان هناك الكثير من الأخطاء ، فعليك تصحيح بعض الأخطاء وحفظها لتكرار عملية التحقق من الصحة. زر الحفظ نشط دائمًا. يتم تطبيق القوائم المرتبطة بالفعل من خلال js ، لكنها معقدة ومربكة. يتم ربط النموذج والعرض ووحدة التحكم بإحكام لذلك قم باختبار كل شيء مجد صعب جدا
كيف تتعامل مع هذا ؟؟ إلى من المثير للاهتمام أنني أسأل تحت القات.
وهكذا لدينا:
لا يعني إنشاء نماذج MVC في شكل كلاسيكي طريقة أخرى للتفاعل مع الخادم مثل التحميل الزائد للصفحة بالكامل ، وهو أمر غير مناسب للمستخدم.
الاستخدام الكامل لأطر العمل مثل Reart و Angular و Vue والانتقال إلى SinglePageApplicatrion من شأنه أن يجعل واجهات أكثر ملاءمة ، لكن لسوء الحظ ، لا يمكن تحقيق ذلك في إطار هذا المشروع لأنه:
- تمت كتابة رمز كثير ، مقبول ، ولن يسمح لك أحد بإعادة ذلك.
-نحن مبرمجون من C # ولا نعرف شبيبة بالقدر المناسب.
بالإضافة إلى ذلك ، يتم توضيح أطر Reart و Angular و Vue لكتابة منطق معقد على العميل ، والذي يبدو غير صحيح من وجهة نظري في WPF. يجب أن يكون كل المنطق في مكان واحد وهذا هو عنصر أعمال و / أو فئة نموذج. عرض يجب أن يعرض فقط حالة النموذج لا أكثر.
بناءً على ما تقدم ، حاولت أن أجد طريقة تسمح لك بالحصول على الحد الأقصى من الوظائف بأقل من كود js. بادئ ذي بدء ، الحد الأدنى للكود الذي يجب كتابته لإخراج وتحديث حقل معين.
تبدو حزمة VueJs + MVC المقترحة على النحو التالي :
- يتم استخدام VueJs في أبسط إصدار مع اتصال عبر CDN. المكونات إذا لزم الأمر نفسه يمكن توصيلها عبر CDN.
- بعد التحميل ، يقوم Vue بتحميل بيانات النموذج من خلال Ajax.
- في كل مرة يتغير فيها النموذج ، ترسل Vue جميع التغييرات إلى الخادم (للحقول النصية ، يمكنك تكوين إرسال التغييرات عند فقد التركيز).
- يحدث التحقق من الصحة على الخادم من خلال آلية الكيان ويتم إرجاع الحقول غير الصالحة إلى العميل وإشارة إلى أن حالة النموذج قد تغيرت فيما يتعلق بقاعدة البيانات.
- إذا حدث طلب التحقق من الصحة التالي قبل الطلب السابق ، فسيتم إلغاء طلب التحقق من الصحة السابق.
لا يتم استخدام نموذج MVC. وظيفة ViewModel بمعنى WPF غير واضحة هنا بين vue ووحدة التحكم.
مزايا مثل هذا التنفيذ على صفحة الشفرة الكلاسيكية: - يتم رسم الواجهة باستخدام أدوات Vue ، المصممة لرسم الواجهات. الميزة الرئيسية.
- فصل طبقات العرض عن ViewModel.
- يتم عرض أخطاء التحقق من صحة كما يملأ النموذج.
- اختبار الراحة
العيوب: - التحميل الزائد على الخادم مع طلبات التحقق من الصحة.
الحاجة إلى معرفة vue و js إلى الحد الأدنى.
أنا أعتبر هذا النهج القالب الأولي للعمل مع النموذج.
في تطبيق حقيقي لنموذج معين ، من المرغوب فيه تحسين:
1) إرسال طلب التحقق من الصحة فقط عند تغيير الحقول ، والتي يجب التحقق من صحة على الخادم.
2) التحقق من الصحة طويل ، الحقول ممتلئة ، إلخ. تعمل على العميل.
لذلك دعونا نذهب.
في المثال الخاص بي ، استخدمت قاعدة بيانات التدريب Northwind ، التي قمت بتنزيلها باستخدام أحد أمثلة Devextreem.
إنشاء التطبيق ، اتصال الكيان وإنشاء DbContext سأترك وراء الكواليس. رابط إلى github بمثال في نهاية المقالة.
إنشاء وحدة تحكم فارغة جديدة MVC 5. نسميها OrdersController. لديها طريقة واحدة حتى الآن.
public ActionResult Index() { return View(); }
أضف واحدة أخرى
public ActionResult Edit() { return View(); }
أنت الآن بحاجة إلى الانتقال إلى مجلد طرق العرض / الطلبات وإضافة صفحتين Index.cshtml و Edit.cshtml
يجب إضافة ملاحظة مهمة تفيد أن صفحة cshtml بدون نموذج يجب أن تضاف إلى أعلى صفحة 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);
يمكنك التحقق من أن كل شيء يعمل عن طريق الكتابة في المستعرض (رقم المنفذ لك بشكل طبيعي) 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;
بعد ذلك ، تحتاج إلى إنشاء نموذج 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 () واتصل بها في القسم المركب:
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: {
حسنًا ، بما أن معرف الكائن يجب أن يأتي الآن من وحدة التحكم ، فأنت في وحدة التحكم تحتاج إلى تمرير المعرف إلى كائن ViewBag الحيوي ، بحيث يمكن الحصول عليه في طريقة العرض.
public ActionResult SimpleEdit(int id = 0) { ViewBag.Id = id; return View(); }
هذا يكفي لقراءة البيانات في وقت التمهيد.
حان الوقت لتخصيص النموذج.
لكي لا أفرط في كتابة المقالة ، استنتجت الحد الأدنى من الحقول. أقترح على المبتدئين معرفة كيفية التعامل مع القوائم المرتبطة.
<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.
ولكن في رمز عرض تم إضافة أكثر من ذلك بكثير.
بالإضافة إلى وظائف البلدان والمدن ، يجب عليك إضافة ساعة تراقب تغييرات الكائن ، لسوء الحظ ، لا يتم حفظ القيمة القديمة لكائن 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: { </script> </body> </html>
موضوع منفصل هو التحقق من الصحة. من وجهة نظر تحسين السرعة ، بالطبع ، تحتاج إلى القيام بالتحقق من صحة العميل. لكن هذا سيؤدي إلى ازدواجية الكود ، لذلك أعرض مثالًا مع التحقق من الصحة على مستوى الكيان (كما ينبغي أن يكون مثاليًا). في الوقت نفسه ، الحد الأدنى من الكود ، يحدث التحقق من الصحة بحد ذاته بسرعة وبشكل غير متزامن أيضًا. كما أظهرت الممارسة ، حتى مع وجود شبكة إنترنت بطيئة للغاية ، فإن كل شيء يعمل أكثر من المعتاد.
تنشأ المشاكل فقط إذا تم كتابة النص بسرعة في حقل النص ، وكانت سرعة الكتابة 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;
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 الخاصة بنا وتعبئتها بكل تغيير في الكائن.
للحصول على أخطاء التحقق من الصحة ، تحتاج إلى تعيين هذا التحقق من الصحة في مكان ما. لقد حان الوقت لإضافة بعض سمات التحقق من الصحة إلى وصفنا لكائن كائن الطلب.
[MinLength(10)] [StringLength(60)] public string ShipAddress { get; set; } [CheckCityAttribute(" ShipCity ")] public string ShipCity { get; set; }
تعد MinLength و StringLength من السمات القياسية ، ولكن بالنسبة إلى ShipCity ، قمت بإنشاء سمة مخصصة
ومع ذلك ، دعنا نترك موضوع التحقق من صحة الكيان أيضًا خارج نطاق هذه المقالة.
لعرض الأخطاء ، تحتاج إلى إضافة رابط إلى 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: { </script> </body> </html>
يبدو 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>
فيما يلي نقطتان مهمتان:
يربط هذا الجزء من الترميز النمط المسؤول عن الإطار الأحمر حول عنصر v-bind: class = "{error:! Errors.Freight == ''} هنا تقوم vue بتوصيل فئة css بحالة.
وهنا تظهر النافذة المنبثقة عندما يكون مؤشر الماوس فوق عنصر:
<span v-if="errors.Freight!==''" class="tooltiptext">{{errors.Freight}}</span>
بالإضافة إلى ذلك ، يجب أن يحتوي العنصر الأصل للعنصر على فئة السمة = "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 ويحتوي على أحدث إصدار. يمكن لأي شخص مهتم بالخيارات الأولية أن يذهب من خلال الالتزامات.
في المثال ، قمت بتطبيق التحسين: إحباط طلب التحقق من الصحة إذا تسبب ، دون انتظار استجابة التحقق من الصحة ، في التحقق من الصحة مرة ثانية.