تعلم ASP.NET MVC


الدرس: TP: جاهزة تقريبا للمطعم


الصفحة السابقة
دعني أخبرك على الفور ، لسنا مستعدين تمامًا لإنهاء طلب اختيار المطعم. نحن نفتقد بعض الأشياء ولكن لدينا بالفعل الكثير من الأساسيات. مع كل هذا ، سنتمكن بالفعل من تقديم طلبنا بشكل ملحوظ. نعم ، هناك الكثير من وحدات التحكم وواجهات العرض التي يجب إدراكها ، والفرصة المتاحة لك للتدريب مع هذا TP الجديد.

التنظيف


مشروعنا الرائع ... مع جميع الاختراقات فيه لتوضيح المفاهيم السابقة ... إنه كراكرا! لقد حان الوقت لتنظيف الربيع ، حتى نتمكن من المغادرة كما ينبغي ومهاجمة TP بهدوء.
في ملف RouteConfig.cs ، يجب أن يكون لدينا المسار الافتراضي فقط ، مع وحدة التحكم  Accueil  الافتراضية:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Accueil", action = "Index", id = UrlParameter.Optional }
    );
}
في دليل وحدة التحكم ، نحتاج إلى وحدة تحكم Accueil تعود إلى طريقة العرض الرئيسية:

public class AccueilController : Controller
{
    public ActionResult Index()
    {
        return View();
    }
}
ثم يجب أن يكون لدينا أيضًا وحدة تحكم Restaurant ، والتي تسمح بإرجاع قائمة جميع المطاعم ، ثم إنشاء وتعديل مطعم:

public class RestaurantController : Controller
{
    private IDal dal;

    public RestaurantController() : this(new Dal())
    {
    }

    public RestaurantController(IDal dalIoc)
    {
        dal = dalIoc;
    }

    public ActionResult Index()
    {
        List listeDesRestaurants = dal.ObtientTousLesRestaurants();
        return View(listeDesRestaurants);
    }

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

    [HttpPost]
    public ActionResult CreerRestaurant(Resto resto)
    {
        if (dal.RestaurantExiste(resto.Nom))
        {
            ModelState.AddModelError("Nom", "Ce nom de restaurant existe déjà");
            return View(resto);
        }
        if (!ModelState.IsValid)
            return View(resto);
        dal.CreerRestaurant(resto.Nom, resto.Telephone);
        return RedirectToAction("Index");
    }

    public ActionResult ModifierRestaurant(int? id)
    {
        if (id.HasValue)
        {
            Resto restaurant = dal.ObtientTousLesRestaurants().FirstOrDefault(r => r.Id == id.Value);
            if (restaurant == null)
                return View("Error");
            return View(restaurant);
        }
        else
            return HttpNotFound();
    }

    [HttpPost]
    public ActionResult ModifierRestaurant(Resto resto)
    {
        if (!ModelState.IsValid)
            return View(resto);
        dal.ModifierRestaurant(resto.Id, resto.Nom, resto.Telephone);
        return RedirectToAction("Index");
    }
}
يجب أن يتكون النموذج من الملفات التالية:
  • BddContext
  • Dal
  • DalEnDur
  • IDal
  • Resto
  • Sondage
  • Utilisateur
  • Vote
كما بالنسبة TP السابق.
يكمن الاختلاف في من Resto   لديه سمات جديدة ولا يجب أن يمتلك أي عقار Email   (إلا إذا كنت تريد بالطبع) :

 [Table("Restos")]
public class Resto
{
    public int Id { get; set; }
    [Required(ErrorMessage = "Le nom du restaurant doit être saisi")]
    public string Nom { get; set; }
    [Display(Name="Téléphone")]
    [RegularExpression(@"^0[0-9]{9}$", ErrorMessage="Le numéro de téléphone est incorrect")]
    public string Telephone { get; set; }
}
إذا اخترت عدم الاحتفاظ بالبريد الإلكتروني ، يجب عليك بالطبع حذف السمة AuMoinsUnDesDeuxAttribute  .
في طرق العرض ، لدينا دليل الصفحة الرئيسية ودليل المطاعم. في الدليل الرئيسي ، لدينا فقط طريقة عرض الفهرس مع عدم وجود شيء خاص به. في دليل المطعم ، لدينا عرض CreerRestaurant.cshtml الذي يحتوي على نموذج إنشاء المطعم. هناك نصوص jQuery و CSS ملحوظة في العلامة <head>   :

<link type="text/css" href="~/Content/Site.css" rel="stylesheet" />
<script type="text/javascript" src="~/Scripts/jquery-1.10.2.js"></script>
<script type="text/javascript" src="~/Scripts/jquery.validate-vsdoc.js"></script>
<script type="text/javascript" src="~/Scripts/jquery.validate.unobtrusive.js"></script>
ثم النموذج في علامة الجسم:

@using (Html.BeginForm())
{
    <fieldset>
        <legend>Ajouter un restaurant</legend>

        <div>
            @Html.LabelFor(model => model.Nom)
            @Html.TextBoxFor(model => model.Nom)
            @Html.ValidationMessageFor(model => model.Nom)
        </div>
        <div>
            @Html.LabelFor(model => model.Telephone)
            @Html.TextBoxFor(model => model.Telephone)
            @Html.ValidationMessageFor(model => model.Telephone)
        </div>
        <br />
        <input type="submit" value="Ajouter" />
    </fieldset>
}
ثم هناك طريقة عرض Index.cshtml التي تحتوي على قائمة المطاعم:

<table>
    <tr>
        <th>Nom</th>
        <th>Téléphone</th>
        <th>Modifier</th>
    </tr>
    @foreach (var resto in Model)
    {
    <tr>
        <td>@resto.Nom</td>
        <td>@resto.Telephone</td>
        <td>@Html.ActionLink("Modifier " + resto.Nom, "ModifierRestaurant", new { id = resto.Id })</td>
    </tr>
    }
</table>
مع وجود القليل من CSS في العلامة <head> :

<style type="text/css">
    table {
        border-collapse: collapse;
    }

    td, th {
        border: 1px solid black;
    }
</style>
ثم لدينا عرض ModifyRestaurant.cshtml الذي يحتوي على نفس JavaScript / CSS مثل العرض CreerRestaurant ، بنفس الشكل تقريبًا:

@using (Html.BeginForm())
{
    <fieldset>
        <legend>Modifier un restaurant</legend>

        <div>
            @Html.LabelFor(model => model.Nom)
            @Html.TextBoxFor(model => model.Nom)
            @Html.ValidationMessageFor(model => model.Nom)
        </div>
        <div>
            @Html.LabelFor(model => model.Telephone)
            @Html.TextBoxFor(model => model.Telephone)
            @Html.ValidationMessageFor(model => model.Telephone)
        </div>
        <br />
        <input type="submit" value="Modifier" />
    </fieldset>
}
يحتوي دليل المشاهدات أيضًا على دليل مشترك لم نلمسه.
هذا كل شيء ، مشروعنا جاهز لتدخلك.

تعليمات


ستفهم ، ستدرك واجهات العرض المختلفة ووحدات التحكم المختلفة لتطبيقنا ، حسنًا ... ليس جميعهم أيضًا ، سأحفظه لوقت لاحق. 
ستقوم أولاً بتحسين عرض الصفحة الرئيسية قليلاً ، وإضافة زر لإنشاء استطلاع ، ثم ارتباطين للانتقال إلى إنشاء مطعم والتشاور مع قائمة المطاعم لهذا الغرض لتعديل الموجود منها. شيء يبدو مثل هذا (حسنًا ، من واجبك أن تجعل شيئًا أجمل مني!):
ASP.NET framework
العرض Accueil
لقد فعلنا بالفعل واجهات العرض وأجهزة التحكم لإضافة وتعديل المطاعم. سيكون عليك فقط اختبار أن كل هذا يعمل في ظروف حقيقية. 
يؤدي إنشاء الاستطلاع إلى إعادة التوجيه إلى وحدة تحكم اتصلت بـ Vote ، والتي تهدف إلى عرض قائمة المطاعم التي سيتمكن المستخدم من التحقق منها للإشارة إلى اختياره:
ASP.NET framework
اختيار مطعم
ملاحظة: يجب أن يكون عنوان URL فريدًا ويحمل معرف الاستطلاع (هنا ، الرقم 1) حتى يتم التواصل مع الناخبين الآخرين الذين نود الذهاب إلى مطعم معهم. سيكون عليك أيضًا التحقق لمعرفة ما إذا كان المستخدم قد صوت بالفعل لمنعه من التصويت مرتين ؛ إذا كانت هذه هي الحالة ، يمكنك على سبيل المثال إعادة توجيهها إلى صفحة النتائج.
بالطبع ، يؤدي التحقق من اختيارك إلى إدراج جميع البيانات في قاعدة البيانات.
سيكون عليك إجبار المستخدم على الاختيار ، وذلك لاختيار مطعم واحد على الأقل. في رأيي ، هذه ليست فكرة رائعة من الناحية الوظيفية ، لأنه من الأفضل السماح للمستخدم بأن يكون مترددًا ، ولكن هنا أريدك أن تقوم بعملية تحقق شخصية ، بالطبع يجب أن يتم ذلك من جانب الخادم ولكن أيضًا من جانب العميل. إذا كان جزء العميل معقدًا للغاية ، فلا يمكنك فعل ذلك لأنني أدرك أنه إذا كنت لا تعرف كيفية عمل جافا سكريبت مع jQuery ، فهذا ليس واضحًا حقًا ؛ خاصة وأنني في هذا البرنامج التعليمي لا أعلمك كيفية إنشاء jQuery ، لذا لا يمكنني حتى أن ألوم.
إذا كنت ترغب في ذلك ، يمكنك إما جعل التحقق يبدو مثل هذا (سهل نسبيًا):
ASP.NET framework
التحقق من صحة الخيارات ، الوضع السهل
أو (أصعب قليلاً):
ASP.NET framework
التحقق من صحة الخيارات ، الوضع الصعب
أنا لا أطلب منك (حتى الآن) إنشاء نظام مصادقة للسماح للمستخدمين بالتمييز. إلا أنه سيكون لديك صعوبة في محاكاة عدة أصوات إذا لم تتمكن من الاتصال بعدة أصوات. ولذلك أقدم اختراقًا مؤقتًا صغيرًا لتحديد مستخدم من اسم المتصفح.
على سبيل المثال ، إذا كنت أستخدم Internet Explorer للتنقل على موقعي ، فستكون الخاصية Request.Browser.Browser تستحق سلسلة الأحرف "IE" . إذا قمت بتصفح الموقع باستخدام Chrome ، فستكون هذه الخاصية تستحق "Chrome" ، إلخ.
حتى نتمكن من كتابة طريقة مؤقتة صغيرة تنشئ أو تعيد المستخدم إلينا باستخدام اسم المتصفح. استبدل الطريقة:

public Utilisateur ObtenirUtilisateur(string idStr)
{
    int id;
    if (int.TryParse(idStr, out id))
        return ObtenirUtilisateur(id);
    return null;
}
من DAL ، من خلال:

public Utilisateur ObtenirUtilisateur(string idStr)
{
    switch (idStr)
    {
        case "Chrome":
            return CreeOuRecupere("Nico", "1234");
        case "IE":
            return CreeOuRecupere("Jérémie", "1234");
        case "Firefox":
            return CreeOuRecupere("Delphine", "1234");
        default:
            return CreeOuRecupere("Timéo", "1234");
    }
}

private Utilisateur CreeOuRecupere(string nom, string motDePasse)
{
    Utilisateur utilisateur = Authentifier(nom, motDePasse);
    if (utilisateur == null)
    {
        int id = AjouterUtilisateur(nom, motDePasse);
        return ObtenirUtilisateur(id);
    }
    return utilisateur;
}
لا تتردد في وضع القيم التي تختارها ، مع المتصفحات التي تختارها.
وبالمثل ، ستحتاج إلى تغيير الطريقة:

public bool ADejaVote(int idSondage, string idStr)
{
    int id;
    if (int.TryParse(idStr, out id))
    {
        Sondage sondage = bdd.Sondages.First(s => s.Id == idSondage);
        if (sondage.Votes == null)
            return false;
        return sondage.Votes.Any(v => v.Utilisateur != null && v.Utilisateur.Id == id);
    }
    return false;
}
بواسطة:

public bool ADejaVote(int idSondage, string idStr)
{
    Utilisateur utilisateur = ObtenirUtilisateur(idStr);
    if (utilisateur != null)
    {
        Sondage sondage = bdd.Sondages.First(s => s.Id == idSondage);
        if (sondage.Votes == null)
            return false;
        return sondage.Votes.Any(v => v.Utilisateur != null && v.Utilisateur.Id == utilisateur.Id);
    }
    return false;
}
حتى تتمكن من معرفة ما إذا كان المتصفح قد صوت بالفعل.
المشكلة مع هذا الاختراق المؤقت هو أنه يكسر أربعة اختبارات DAL تلقائية. نظرًا لأننا سنصلح هذه الأساليب قريبًا ، عندما يكون لدينا المزيد من المعرفة ، يمكنك ترك الاختبارات الأربعة تفشل في الوقت الحالي. نحن لا نصححهم لأن هذه هي النتيجة التي نرغب في الحصول عليها. عندما نجري تغييراتنا ، يجب أن تعود الاختبارات إلى اللون الأخضر.
يبقى فقط تحقيق المراقب والرأي الذي يسمح بالحصول على نتائج التصويت ، بحيث يكون:
ASP.NET framework
عرض نتيجة التصويت
بالطبع لا يمكن مشاهدة هذه الصفحة حتى نصوت ...
إذا كنت ترغب في ذلك ، فلا تتردد في القفز وتخطي الفصل التالي. ومع ذلك ، هناك بعض النقاط المعقدة التي سأذكرها بالتفصيل في هذه الخطوة ...

بعض التفاصيل الإضافية


توجد أشياء حساسة قليلاً في الصفحة تسمح لك بالتحقق من صحة تصويتك. توجد بالفعل خانات اختيار. كما رأينا ، يتم إنشاء خانات الاختيار من المساعد Html.CheckBox   (وصديقه Html.CheckBoxFor  ) ويجب ربطها بمنطقية للاستفادة من ربط النموذج.
ومع ذلك ، لدينا قائمة من المطاعم تحت تصرفنا ... لذلك لا يوجد منطقية. أسهل طريقة لاستخدام ربط النموذج هي إنشاء نموذج عرض يحتوي على معرف المطعم (للعثور عليه بطريقة فريدة) ، واسم ورقم هاتف المطعم ، بالإضافة إلى منطقي يسمح بمعرفة تم فحصه أم لا. باختصار ، نموذج عرض (view-model) مثل هذا:

public class RestaurantCheckBoxViewModel
{
    public int Id { get; set; }
    public string NomEtTelephone { get; set; }
    public bool EstSelectionne { get; set; }
}
يحمل نفسه بنموذج عرض (view-model ) آخر سيتم ربطه بالعرض. لماذا نموذج عرض آخر؟ لأنه في حالتي ، اخترت تنفيذ التحقق المخصص باستخدام الواجهة IValidatableObject  . لذلك ، من الطبيعي جدًا أن يتم تطبيق نموذج العرض هذا:

public class RestaurantVoteViewModel : IValidatableObject
{
    public List<RestaurantCheckBoxViewModel> ListeDesResto { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        // à faire !
    }
}
إذا كنت لا تريد استخدام هذا الحل لإنشاء مدقق مخصص ، فيمكنك تخطي هذا الفصل.
سمحت لك باختيار النظام الخاص بك لتطبيق التحقق من صحة الخادم الخاص بك ، ولكن يمكنني الآن أن أقدم لك الحل للتحقق من قائمة المطاعم على جانب العميل. في الواقع ، كما قلت لك ، قد يكون من الطبيعي ألا يكون لديك جميع أفكار jQuery للنجاح في القيام بهذا النوع من عمليات التحقق. لن ألومك إذا نسخت القليل حول ما فعلته إذا لم تتقن jQuery .  (من ناحية أخرى ، إذا كنت من محبي jQuery ، فلا يوجد لديك ما تقرأه هنا) :

<script type="text/javascript">
jQuery.validator.addMethod("verifListe", function (value, element, params) {
    var nombreCoche = $('input:checked[data-val-verifListe]').length;
    if (nombreCoche == 0) {
        $('span[data-valmsg-for=ListeDesResto]').text(params.message).removeClass("field-validation-valid").addClass("field-validation-error");
    }
    else {
        $('span[data-valmsg-for=ListeDesResto]').text('');
    }
    return nombreCoche != 0;
});

jQuery.validator.unobtrusive.adapters.add
    ("verifListe", function (options) {
        options.params.message = options.message;
        options.rules["verifListe"] = options.params;
        options.messages["verifListe"] = options.message;
    });
</script>
لقد اتصلت بطريقة التحقق الخاصة بي verifListe وأستخدم محدد jQuery لاسترداد جميع المربعات التي تم تحديدها والتي لها سمة data-val-verifListe . وإذا لم يتم تحديد أي من المربعات ، فأنا استخدم حقل التحقق لعرض رسالة الخطأ الخاصة بي.
هذا كل ما ستحصل عليه مني الآن. لقد حان الوقت للبدء في هذا البرنامج الفني الذي ليس معقدًا للغاية ، ولكنه لا يزال يتطلب فهم جميع المبادئ التي شرحتها لك سابقًا.

تصحيح


فكيف كان ذلك؟ سهل ، ليس سهلاً؟ أصابع في الأنف أتخيل ...
حان الوقت لأثبت لك أنك لست الوحيد الذي يعمل وأنني استغرقت بعض الوقت للقيام بهذا TP . 
أعتقد أنه لم يكن صعبًا بشكل عام ، ربما باستثناء الجزء المتعلق بالتحقق المخصص ؛ لكنك احتجت إلى تحدٍ صغير ، وإلا لما حاولت ذلك. هيا ، دعنا نذهب إلى التصحيح.
لنبدأ مع طريقة العرض الرئيسية ، يجب أن تعرض ببساطة زرًا لإنشاء استطلاع ، بالإضافة إلى رابط إلى الإجراءين لإضافة مطعم وتعديله:

<p>Prêts à choisir un restaurant ?</p>
@using (Html.BeginForm())
{
    <input type="submit" value="Créer un sondage" />
}
<ul>
    <li>@Html.ActionLink("Ajouter un restaurant", "CreerRestaurant", "Restaurant")</li>
    <li>@Html.ActionLink("Modifier les restaurants", "Index", "Restaurant")</li>
</ul>
بالطبع ، نستخدم المساعد ActionLink   لإنشاء رابط النص التشعبي لإجراء من قبل وحدة تحكم. Accueil  لا تفعل وحدة التحكم الكثير:

public class AccueilController : Controller
{
    private IDal dal;

    public AccueilController() : this(new Dal())
    {
    }

    public AccueilController(IDal dalIoc)
    {
        dal = dalIoc;
    }

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

    [HttpPost]
    [ActionName("Index")]
    public ActionResult IndexPost()
    {
        int idSondage = dal.CreerUnSondage();
        return RedirectToAction("Index", "Vote", new { id = idSondage });
    }
}
انتهزت الفرصة لإعادة تشكيل وحدة التحكم قليلاً لتضمين حقن التبعية الضعيفة. خلافًا لذلك ، ليس كثيرًا في وحدة التحكم هذه ، هناك ما يكفي فقط للرد على إرسال النموذج لإنشاء الاستطلاع.
أستخدم هنا الحيلة المعروضة في الفصل السابق والتي تتكون من إعطاء اسم مختلف لطريقة التحكم في الإجراء الذي من المفترض أن تديره. هذا لأن أسلوب GET الخاص بي لديه نفس التوقيع مثل طريقة POST الخاصة بي.
يستدعي هذا الإجراء فقط طريقة DAL ، ثم يمرر معرف الاستطلاع الذي تم إنشاؤه حديثًا إلى وحدة التحكم التي تدير الأصوات.
ثم انتقل إلى وحدة التحكم Vote  . أول شيء يجب فعله هو إنشاء نماذج العرض التي قدمتها لك في الفصل السابق:

public class RestaurantCheckBoxViewModel
{
    public int Id { get; set; }
    public string NomEtTelephone { get; set; }
    public bool EstSelectionne { get; set; }
}
و

public class RestaurantVoteViewModel : IValidatableObject
{
    public List<RestaurantCheckBoxViewModel> ListeDesResto { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (!ListeDesResto.Any(r => r.EstSelectionne))
            yield return new ValidationResult("Vous devez choisir au moins un restaurant", new[] { "ListeDesResto"});
    }
}
كما قلت لك ، اخترت تنفيذ التحقق المخصص من خلال التنفيذ IValidatableObject  . لقد اتخذت هذا الخيار لأن هذا التنفيذ مرتبط ارتباطًا وثيقًا ببنية نموذج العرض الخاص بي ، وخاصةً بخاصيته EstSelectionne  . إذا كنت قد أنشأت سمة ، لكانت قد تم ربطها أيضًا بنموذج العرض هذا ، مما يجعلها تفقد إمكانية إعادة استخدامها. لذا ، في هذه الحالة ، بقدر ما يتعلق بالسمة لأنه لا يمكن إعادة استخدامها في مكان آخر من هذا المشروع.
هنا ،الطريقة Validate تعرض خطأ إذا لم يتم تحديد مطعم على الإطلاق. ويرتبط هذا الخطأ بالملكية ListeDesResto  . وهذا يعني ضمنيًا أنه يجب عليّ وضع مساعد للتحقق مرتبط بهذه الخاصية للنموذج. سترى في طريقة العرض.
وحدة التحكم كما يلي:

public class VoteController : Controller
{
    private IDal dal;

    public VoteController() : this(new Dal())
    {
    }

    public VoteController(IDal dalIoc)
    {
        dal = dalIoc;
    }

    public ActionResult Index(int id)
    {
        RestaurantVoteViewModel viewModel = new RestaurantVoteViewModel
            {
                ListeDesResto = dal.ObtientTousLesRestaurants().Select(r => new RestaurantCheckBoxViewModel { Id = r.Id, NomEtTelephone = string.Format("{0} ({1})", r.Nom, r.Telephone)}).ToList()
            };
        if (dal.ADejaVote(id, Request.Browser.Browser))
        {
            return RedirectToAction("AfficheResultat", new { id = id });
        }
        return View(viewModel);
    }

    [HttpPost]
    public ActionResult Index(RestaurantVoteViewModel viewModel, int id)
    {
        if (!ModelState.IsValid)
            return View(viewModel);
        Utilisateur utilisateur = dal.ObtenirUtilisateur(Request.Browser.Browser);
        if (utilisateur == null)
            return new HttpUnauthorizedResult();
        foreach (RestaurantCheckBoxViewModel restaurantCheckBoxViewModel in viewModel.ListeDesResto.Where(r => r.EstSelectionne))
        {
            dal.AjouterVote(id, restaurantCheckBoxViewModel.Id, utilisateur.Id);
        }
        return RedirectToAction("AfficheResultat", new { id = id });
    }

    public ActionResult AfficheResultat(int id)
    {
        if (!dal.ADejaVote(id, Request.Browser.Browser))
        {
            return RedirectToAction("Index", new { id = id });
        }
        List<Resultats> resultats = dal.ObtenirLesResultats(id);
        return View(resultats.OrderByDescending(r => r.NombreDeVotes).ToList());
    }
}
في إجراء index ، ننشئ نموذج العرض من قائمة المطاعم. ثم نتحقق من أن المستخدم لم يصوت بالفعل. كما هو موضح ، يتم استخدام الخاصية (مؤقتًا) Request.Browser.Browser   لتحديد المستخدم بشكل فريد.
ثم هناك إجراء index إلى POST الذي تم استدعاؤه عند إرسال النموذج. نتحقق بالطبع من أن النموذج صالح ، ثم نتحقق أيضًا من أننا نقوم باسترداد مستخدم. إذا لم يكن الأمر كذلك ، فأنا أعيد الخطأ 401. بالطبع ، لن يحدث ذلك هنا أبدًا ، ولكنه توقعًا لآلية مصادقة مستقبلية ... وبعد ذلك ، أضفت المطاعم التي تم اختيارها إلى أصوات الاستطلاع.
سترى أيضًا إجراء عرض النتائج ، والذي سيسمح لك باسترداد النتائج وإرسالها مرة أخرى إلى عرض العرض الذي سأقدمه بعد عرض التصويت ، وهو على النحو التالي:

@model  ChoixResto.ViewModels.RestaurantVoteViewModel
<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
    <link type="text/css" href="~/Content/Site.css" rel="stylesheet" />
    <script type="text/javascript" src="~/Scripts/jquery-1.10.2.js"></script>
    <script type="text/javascript" src="~/Scripts/jquery.validate-vsdoc.js"></script>
    <script type="text/javascript" src="~/Scripts/jquery.validate.unobtrusive.js"></script>

    <script type="text/javascript">
        jQuery.validator.addMethod("verifListe", function (value, element, params) {
            var nombreCoche = $('input:checked[data-val-verifListe]').length;
            if (nombreCoche == 0) {
                $('span[data-valmsg-for=ListeDesResto]').text(params.message).removeClass("field-validation-valid").addClass("field-validation-error");
            }
            else {
                $('span[data-valmsg-for=ListeDesResto]').text('');
            }
            return nombreCoche != 0;
        });

        jQuery.validator.unobtrusive.adapters.add
            ("verifListe", function (options) {
                options.params.message = options.message;
                options.rules["verifListe"] = options.params;
                options.messages["verifListe"] = options.message;
            });
    </script>
</head>
<body>
    <p>Cochez les restaurants où vous voulez bien aller. Attention, le vote est définitif !</p>
    @Html.ValidationMessageFor(m => m.ListeDesResto)
    @using (Html.BeginForm())
    {
        for (int i = 0; i < Model.ListeDesResto.Count; i++)
        {
            <div>
                @Html.CheckBoxFor(m => m.ListeDesResto[i].EstSelectionne, new { data_val = "true", data_val_verifListe = "Vous devez choisir au moins un restaurant" })
                @Html.LabelFor(m => m.ListeDesResto[i].EstSelectionne, Model.ListeDesResto[i].NomEtTelephone)
                @*@Html.ValidationMessageFor(m => m.ListeDesResto[i].EstSelectionne)*@
                @Html.HiddenFor(m => m.ListeDesResto[i].Id)
                @Html.HiddenFor(m => m.ListeDesResto[i].NomEtTelephone)
            </div>
        }
        <input type="submit" value="Valider le choix" style="margin-top: 20px;" />
    }
</body>
</html>
إنه الأكثر تعقيدًا. يمكنك أن ترى في العلامة <head>   شوائب CSS و jQuery المختلفة التي نحتاجها لإجراء عمليات التحقق. أمضي في هذه اللحظة طريقة التحقق الشخصية ، وسأوضحها أكثر قليلاً.
في الجزء المتبقي من العرض ، أبدأ بوضع موضع رسالة الخطأ الخاصة بالتحقق من الصحة على جانب الخادم ، والتي تحملها خاصية ListeDesResto   نموذج العرض الخاص بي:

@Html.ValidationMessageFor(m => m.ListeDesResto)
هذا مهم لأننا نحتاج إلى مكان لعرض الخطأ في حالة تعطيل المستخدم جافا سكريبت وفي الحالة (غير مرجحة) حيث نكون كسالى جدًا لإجراء التحقق من الصحة من جانب العميل.
ثم ، نعرض الخيارات المختلفة في خانات الاختيار بفضل المساعد:

@Html.CheckBoxFor(m => m.ListeDesResto[i].EstSelectionne, new { data_val = "true", data_val_verifListe = "Vous devez choisir au moins un restaurant"})
أغتنم هذه الفرصة لإنشاء السمات التي سيتم استخدامها للتحقق من صحة العميل. لاحظ أن التصنيف مرتبط بنفس عنصر خانة الاختيار حتى نتمكن من تحديد / إلغاء تحديد المربع عن طريق النقر أيضًا على الملصق.
إذا كنت تريد التأكد من عرض خطأ التحقق بجوار كل خانة اختيار ، يجب عليك وضع ValidationMessage على نفس خاصية النموذج وإضافة السطر الذي علقت عليه (سأتحدث عن التعليقات في الجزء التالي):

@Html.ValidationMessageFor(m => m.ListeDesResto[i].EstSelectionne)
باستخدام رسالة التحقق هذه ، لإجراء التحقق من صحة العميل ، ما عليك سوى حساب عدد خانات الاختيار وإرجاع خطأ حتى تظهر الرسائل. خلاف ذلك ، للحصول على رسالة واحدة فقط - مثل ما فعلته - عليك القيام بأشياء إضافية لعرض رسالة الخطأ في الحقل المرتبط بـ ListeDesResto  .
دعنا نتابع النموذج ونلاحظ أنني أضفت حقولًا مخفية من أجل توفير العناصر لربط النموذج حتى يتمكن من إعادة تكوين نموذج العرض الخاص بنا عند إرسال النموذج.
لذلك ، عندما يحلل ربط النموذج محتوى طلب POST ، سيجد لكل مطعم معرفًا ( حقل النوع hidden  ) واسمًا وهاتفًا (حقل النوع hidden ، وهو هنا غير مفيد) وإذا كان تم تحديده أم لا (بفضل خانة الاختيار) .
الآن دعونا نتحدث قليلاً عن التحقق من صحة العميل. هنا هو جافا سكريبت الذي صنعته:

<script type="text/javascript">
    jQuery.validator.addMethod("verifListe", function (value, element, params) {
        var nombreCoche = $('input:checked[data-val-verifListe]').length;
        if (nombreCoche == 0) {
            $('span[data-valmsg-for=ListeDesResto]').text(params.message).removeClass("field-validation-valid").addClass("field-validation-error");
        }
        else {
            $('span[data-valmsg-for=ListeDesResto]').text('');
        }
        return nombreCoche != 0;
    });

    jQuery.validator.unobtrusive.adapters.add
        ("verifListe", function (options) {
            options.params.message = options.message;
            options.rules["verifListe"] = options.params;
            options.messages["verifListe"] = options.message;
        });
</script>
المبدأ هو الحصول على عدد المربعات المختارة باستخدام محدد jQuery أن يعمل من خلال جميع مدخلات فحص ( input:checked ) وجود رقابة السمة الخاصة التي تدعمها آلية التحقق: data-val-verifListe  .
إذا كنت أستخدم مساعد التحقق من الصحة المرتبط بالخاصية EstSelectionne   الذي يقوم بإنشاء العديد من رسائل الخطأ كما هو الحال في المربعات ، فيجب عليّ فقط إرجاع القيمة الصحيحة إذا كان عدد العناصر المحددة غير 0. عادةً هذا سيكون :

<script type="text/javascript">
    jQuery.validator.addMethod("verifListe", function (value, element, params) {
        var nombreCoche = $('input:checked[data-val-verifListe]').length;
        return nombreCoche != 0;
    });

    jQuery.validator.unobtrusive.adapters.add
        ("verifListe", function (options) {
            options.rules["verifListe"] = options.params;
            options.messages["verifListe"] = options.message;
        });
</script>
باستثناء أنني اخترت عدم عرض الرسالة X مرات ولكن مرة واحدة فقط في الحقل المرتبط بـ ListeDesResto ، نفس الرسالة المستخدمة للتحقق من جانب الخادم. لتحديده ، يمكنني استخدام المحدد التالي:

$('span[data-valmsg-for=ListeDesResto]')
مما يسمح لي بالوصول إلى span   يملك شخصًا attribut data-valmsg-for  جديرًا ListeDesResto  . للعثور على هذا ، كان علي بالطبع أن أبحث في كود المصدر للصفحة. لذا فإن المساعد:

@Html.ValidationMessageFor(m => m.ListeDesResto)
يولد لي:

<span class="field-validation-valid" data-valmsg-for="ListeDesResto" data-valmsg-replace="true"></span>
بمجرد أن يقوم المحدد بإرجاع العنصر الخاص بي ، يمكنني تعيين قيمة رسالة الخطأ التي مررتها في المُدخل ، باستخدام طريقة النص (...). ثم لكي يتم عرضه باللون الأحمر ، أقوم بتغيير فئة CSS وأنتقل منه field-validation-valid  إلى field-validation-error  أضع رسائل الخطأ على وجه الخصوص باللون الأحمر. إذا تم تحديد مربع واحد على الأقل ، فقم بمسح رسالة الخطأ من خلال تعيين قيمة فارغة لعنصر تحكم HTML .
وهذا كل شيء من أجل التحقق.
أنا أدرك أنه ليس من الواضح بالضرورة أن تلعب بهذا ، لكنها ممارسة جيدة وآمل أن تكون قد نجحت ، أو إذا فشلت في ذلك ، آمل أن تكون قد فهمت تصحيحي.
يبقى فقط العرض الذي يعرض النتائج. إنها بسيطة للغاية:

@model  List

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Voir les résultats</title>
    <style type="text/css">
        table {
            border-collapse: collapse;
        }

        td, th {
            border: 1px solid black;
        }
    </style>
</head>
<body>
    <p>Résultats du sondage :</p>
    <table>
        <tr>
            <th>Nom</th>
            <th>Téléphone</th>
            <th>Nombre de votes</th>
        </tr>
        @foreach (var resto in Model)
        {
        <tr>
            <td>@resto.Nom</td>
            <td>@resto.Telephone</td>
            <td>@resto.NombreDeVotes</td>
        </tr>
        }
    </table>
</body>
</html>
عليك فقط عرض النتائج ، باستخدام طريقة عرض مكتوبة بشدة حيث يكون النموذج قائمة بالنتائج. لا الفخاخ.
انتهى هذا البرنامج التعليمي.
آمل أن يكون قد خدمك جيدًا لاستيعاب المفاهيم المتعلقة بأجهزة التحكم والآراء والتحقق من البيانات. هذا هو حقا الأساس لأي تطوير ASP.NET MVC .

والاختبارات؟


لقد اختبرت بالتأكيد أن كل شيء يعمل عن طريق بدء المتصفح الخاص بك ، وإنشاء العديد من المطاعم والتحقق من صوتك. ثم قمت بتشغيل متصفحات أخرى (أو أكثر) لمحاكاة تصويت مستخدم آخر من عنوان URL للاستطلاع.
(إلى جانب ذلك ، أدركت أن المستخدم الذي صوت أولاً أجبر على تحديث صفحته لرؤية تصويت المستخدم الآخر ، سنقوم بتصحيح ذلك في الوقت المناسب ...)
ولكن هل فكرت في إجراء الاختبارات الآلية لوحدة التحكم الجديدة لدينا؟ والإجراءات الجديدة من وحدة تحكم الاستقبال لدينا؟
إذا كانت الإجابة بنعم ، فتهانينا. ستضمن هذه الاختبارات أن كل شيء يستمر في العمل بشكل جيد مع التطورات والتصحيحات المحتملة.
إذا لم يكن كذلك ، فأنت تعرف ما عليك القيام به. 
أقدم هنا الاختبارات القليلة التي قمت بها للتاريخ لمعرفة ما إذا كان لديك نفس الشيء تقريبًا. بالفعل ، يجب علينا اختبار عمل Index   وحدة التحكم Accueil  ، عندما يتم استدعاؤها في POST :

 [TestMethod]
public void AccueilController_IndexPost_RenvoiActionVote()
{
    using (IDal dal = new DalEnDur())
    {
        AccueilController controller = new AccueilController(dal);

        RedirectToRouteResult resultat = (RedirectToRouteResult)controller.IndexPost();

        Assert.AreEqual("Index", resultat.RouteValues["action"]);
        Assert.AreEqual("Vote", resultat.RouteValues["controller"]);
        List<Resultats> resultats = dal.ObtenirLesResultats(1);
        Assert.IsNotNull(resultats);
    }
}
لم يكن لديك الكثير من المشاكل مع مثل هذا الاختبار.
الآن علينا أن نختبر جهاز التحكم Vote   وإجراءاته المختلفة. لهذا ، أنشأت فئة جديدة من الاختبارات التي اتصلت بها VoteControllerTests  . أول شيء فعلته هو إنشاء مجموعة بيانات صغيرة في بداية كل اختبار ، مع العلم أنني بالطبع DalEnDur   أستخدمها كقابس:

 [TestClass]
public class VoteControllerTests
{
    private IDal dal;
    private int idSondage;

    [TestInitialize]
    public void Init()
    {
        dal = new DalEnDur();
        idSondage = dal.CreerUnSondage();
    }

    [TestCleanup]
    public void Clean()
    {
        dal.Dispose();
    }
}
لذا ، قبل كل اختبار ، أنا متأكد من إجراء مسح. لا يزال لدي أيضًا المطاعم الثلاثة التي تم تهيئتها في الشركة المصنعة لـ DalEnDur ، حيث يمكنني أيضًا إنشاء الاستقصاء الخاص بي ، ولكن ربطها فقط باختبارات التصويت ، هذا لا يهم.
ملاحظة: لا تنس استدعاء طريقة Dispose  من DalEnDur لتنظيف الذاكرة بين كل اختبار.
قبل الذهاب إلى الاختبارات بأنفسنا ، سيتعين علينا أن نلعب مع Moq ، رفيقنا المخلص. في الواقع ، في وحدة التحكم الخاصة بنا ، نستخدم خدعة خاصة إلى حد ما لمحاكاة اسم المستخدم: الوصول إلى اسم المتصفح:

Utilisateur utilisateur = dal.ObtenirUtilisateur(Request.Browser.Browser);
ليس الأمر قبيحًا فحسب ، بل يجبرنا أيضًا على سد هذا الإدمان. نعم ، عادةً ما يكون كائن الطلب عبارة عن كائن تم ملؤه بواسطة ASP.NET وليس له أي سبب ليكون في سياق الاختبارات التلقائية.
هذا القبح سيظل له تأثير إيجابي: اسمح لنا بفهم القليل عن كيفية إنشاء هذا الكائن الشهير Request   وبشكل أكثر دقة ، كيف يتم إنشاء سياق HTTP ، عبر الفصل HttpContextBase  .
يتم تعيين هذا بالفعل عبر خاصية ControllerContext   أي وحدة تحكم. وبالتالي ، هذه هي الطريقة التي سنتمكن من تعيين سياق HTTP خاطئ لها.
على سبيل المثال ، لتوصيل خاصية المتصفح ، سأتمكن من القيام بما يلي:

Mock<ControllerContext> controllerContext = new Mock<ControllerContext>();
controllerContext.Setup(p => p.HttpContext.Request.Browser.Browser).Returns("1");

VoteController controleur = new VoteController(dal);
controleur.ControllerContext = controllerContext.Object;
هنا ، عند استخدام خاصية المتصفح ، سيكون لدينا السلسلة "1" بدلاً من ذلك ، ثم تحويلها إلى عدد صحيح والحصول على معرف المستخدم. يجب أن يتم هذا التوصيل لكل طريقة اختبار ، وأضيفه في طريقة التهيئة:

 [TestClass]
public class VoteControllerTests
{
    private IDal dal;
    private int idSondage;
    private VoteController controleur;

    [TestInitialize]
    public void Init()
    {
        dal = new DalEnDur();
        idSondage = dal.CreerUnSondage();

        Mock<ControllerContext> controllerContext = new Mock<ControllerContext>();
        controllerContext.Setup(p => p.HttpContext.Request.Browser.Browser).Returns("1");

        controleur = new VoteController(dal);
        controleur.ControllerContext = controllerContext.Object;
    }

    [TestCleanup]
    public void Clean()
    {
        dal.Dispose();
    }
}
تبقى الاختبارات نفسها.
نبدأ باختبار إجراء index باختبارين سيتيحان لنا التحقق من حصولنا على نموذج عرض متماسك ، أولاً دون إنشاء مستخدم (أي أن التوصيل سيوفر معرف غير موجود في قائمة المستخدمين) ، ثم مع مستخدم لم يصوت:

 [TestMethod]
public void Index_AvecSondageNormalMaisSansUtilisateur_RenvoiLeBonViewModelEtAfficheLaVue()
{
    ViewResult view = (ViewResult)controleur.Index(idSondage);

    RestaurantVoteViewModel viewModel = (RestaurantVoteViewModel)view.Model;
    Assert.AreEqual(3, viewModel.ListeDesResto.Count);
    Assert.AreEqual(1, viewModel.ListeDesResto[0].Id);
    Assert.IsFalse(viewModel.ListeDesResto[0].EstSelectionne);
    Assert.AreEqual("Resto pinambour (0102030405)", viewModel.ListeDesResto[0].NomEtTelephone);
}

[TestMethod]
public void Index_AvecSondageNormalAvecUtilisateurNayantPasVote_RenvoiLeBonViewModelEtAfficheLaVue()
{
    dal.AjouterUtilisateur("Nico", "1234");
    dal.AjouterUtilisateur("Jérémie", "1234");

    ViewResult view = (ViewResult)controleur.Index(idSondage);

    RestaurantVoteViewModel viewModel = (RestaurantVoteViewModel)view.Model;
    Assert.AreEqual(3, viewModel.ListeDesResto.Count);
    Assert.AreEqual(1, viewModel.ListeDesResto[0].Id);
    Assert.IsFalse(viewModel.ListeDesResto[0].EstSelectionne);
    Assert.AreEqual("Resto pinambour (0102030405)", viewModel.ListeDesResto[0].NomEtTelephone);
}
هذان الاختباران كلاسيكيان للغاية ولا يجلبان أي شيء جديد. دعونا أيضًا نختبر الحالة التي قام فيها المستخدم بإجراء تصويت بالفعل ويجب إعادة توجيهه لعرض النتائج:

 [TestMethod]
public void Index_AvecSondageNormalMaisDejaVote_EffectueLeRedirectToAction()
{
    dal.AjouterUtilisateur("Nico", "1234");
    dal.AjouterUtilisateur("Jérémie", "1234");
    dal.AjouterVote(idSondage, 1, 1);

    RedirectToRouteResult resultat = (RedirectToRouteResult)controleur.Index(idSondage);

    Assert.AreEqual("AfficheResultat", resultat.RouteValues["action"]);
    Assert.AreEqual(idSondage, resultat.RouteValues["id"]);
}
ثم دعنا نذهب إلى إجراء index ولكن في POST . يمكننا التحقق من أن نموذج العرض غير الصالح يُرجع طريقة العرض الافتراضية بنفس طراز العرض:

 [TestMethod]
public void IndexPost_AvecViewModelInvalide_RenvoiLeBonViewModelEtAfficheLaVue()
{
    RestaurantVoteViewModel viewModel = new RestaurantVoteViewModel
    {
        ListeDesResto = new List<RestaurantCheckBoxViewModel>
            {
                new RestaurantCheckBoxViewModel { EstSelectionne = false, Id = 2, NomEtTelephone = "Resto pinière (0102030405)"},
                new RestaurantCheckBoxViewModel { EstSelectionne = false, Id = 3, NomEtTelephone = "Resto toro (0102030405)"},
            }
    };
    controleur.ValideLeModele(viewModel);

    ViewResult view = (ViewResult)controleur.Index(viewModel, idSondage);

    viewModel = (RestaurantVoteViewModel)view.Model;
    Assert.AreEqual(2, viewModel.ListeDesResto.Count);
    Assert.AreEqual(2, viewModel.ListeDesResto[0].Id);
    Assert.IsFalse(viewModel.ListeDesResto[0].EstSelectionne);
    Assert.AreEqual("Resto pinière (0102030405)", viewModel.ListeDesResto[0].NomEtTelephone);
}
هذا أيضًا هو l’archi-classique-déjà-vu بالفعل. يُرجع هذا الإجراء نفسه أيضًا رمز الخطأ 401 إذا كان نموذج العرض لا يزال صالحًا ولكن لم يتم العثور على المستخدم. يمكننا اختباره على النحو التالي:

 [TestMethod]
public void IndexPost_AvecViewModelValideMaisPasDutilisateur_RenvoiUneHttpUnauthorizedResult()
{
    RestaurantVoteViewModel viewModel = new RestaurantVoteViewModel
    {
        ListeDesResto = new List<RestaurantCheckBoxViewModel>
            {
                new RestaurantCheckBoxViewModel { EstSelectionne = true, Id = 2, NomEtTelephone = "Resto pinière (0102030405)"},
                new RestaurantCheckBoxViewModel { EstSelectionne = false, Id = 3, NomEtTelephone = "Resto toro (0102030405)"},
            }
    };
    controleur.ValideLeModele(viewModel);

    HttpUnauthorizedResult view = (HttpUnauthorizedResult)controleur.Index(viewModel, idSondage);

    Assert.AreEqual(401, view.StatusCode);
}
أخيرًا ، يمكننا التحقق من أن كل شيء يسير على ما يرام باستخدام نموذج عرض صالح ومستخدم حاضر:

 [TestMethod]
public void IndexPost_AvecViewModelValideEtUtilisateur_AppelleBienAjoutVoteEtRenvoiBonneAction()
{
    Mock<IDal> mock = new Mock<IDal>();
    mock.Setup(m => m.ObtenirUtilisateur("1")).Returns(new Utilisateur { Id = 1, Prenom = "Nico" });

    Mock<ControllerContext> controllerContext = new Mock<ControllerContext>();
    controllerContext.Setup(p => p.HttpContext.Request.Browser.Browser).Returns("1");
    controleur = new VoteController(mock.Object);
    controleur.ControllerContext = controllerContext.Object;

    RestaurantVoteViewModel viewModel = new RestaurantVoteViewModel
    {
        ListeDesResto = new List<RestaurantCheckBoxViewModel>
                {
                    new RestaurantCheckBoxViewModel { EstSelectionne = true, Id = 2, NomEtTelephone = "Resto pinière (0102030405)"},
                    new RestaurantCheckBoxViewModel { EstSelectionne = false, Id = 3, NomEtTelephone = "Resto toro (0102030405)"},
                }
    };
    controleur.ValideLeModele(viewModel);

    RedirectToRouteResult resultat = (RedirectToRouteResult)controleur.Index(viewModel, idSondage);

    mock.Verify(m => m.AjouterVote(idSondage, 2, 1));
    Assert.AreEqual("AfficheResultat", resultat.RouteValues["action"]);
    Assert.AreEqual(idSondage, resultat.RouteValues["id"]);
}
لاحظ هنا أنني أضفت المزيد لتظهر لك قوة Moq . عن طريق توصيل Dal بال Moq ، يمكنني التحقق من أنه تم استدعاء طريقة. هنا في هذه الحالة ، أريد التحقق من أن طريقة AjouterVote   LAD قد تم استدعاءها بالفعل. يتم ذلك مع الطريقة Verify  . من ناحية أخرى ، أنا ملزم في بداية الاختبار بإعادة جميع عمليات التهيئة الخاصة بي لأنها مختلفة عن تلك التي تم إجراؤها في طريقة التهيئة.
أخيرًا ، يبقى اختبار طريقة عرض النتائج ، دون التصويت مسبقًا والتصويت بشكل صحيح:

 [TestMethod]
public void AfficheResultat_SansAvoirVote_RenvoiVersIndex()
{
    RedirectToRouteResult resultat = (RedirectToRouteResult)controleur.AfficheResultat(idSondage);

    Assert.AreEqual("Index", resultat.RouteValues["action"]);
    Assert.AreEqual(idSondage, resultat.RouteValues["id"]);
}

[TestMethod]
public void AfficheResultat_AvecVote_RenvoiLesResultats()
{
    dal.AjouterUtilisateur("Nico", "1234");
    dal.AjouterUtilisateur("Jérémie", "1234");
    dal.AjouterVote(idSondage, 1, 1);

    ViewResult view = (ViewResult)controleur.AfficheResultat(idSondage);

    List<Resultats> model = (List<Resultats>)view.Model;
    Assert.AreEqual(1, model.Count);
    Assert.AreEqual("Resto pinambour", model[0].Nom);
    Assert.AreEqual(1, model[0].NombreDeVotes);
    Assert.AreEqual("0102030405", model[0].Telephone);
}
كثيرًا لهذه النظرة العامة الصغيرة على اختباراتي. يمكن أن تكون أكثر شمولاً من ذلك لذا لا تتردد في إثرائها بأفضل ما يمكن.
من المهم دائمًا التعود على كتابة اختبار آلي للتحقق من صحة التطوير. ستشكر نفسك لاحقًا عندما تحتاج إلى الرجوع إلى التعليمات البرمجية ... أو سيباركك زملاؤك داخليًا لتركهم الاختبارات في حالة جيدة. 

الاستنتاج العام للعبة


هذا كل شيء ، نحن نعلم عند أطراف الأصابع أسس MVC ، وهي النموذج والمنظور ووحدة التحكم. لقد رأينا أن كل عنصر من MVC يشكل مجموعة كاملة وقوية جدًا. يضاف إلى ذلك عناصر أخرى تشكل جزءًا لا يتجزأ من قلب النظام الأساسي ، مثل الطرق. سنكتشف في القسم التالي أن العناصر الإضافية الأخرى ستثري تطبيقات الويب الخاصة بنا وتجعلنا نكتشف كل قوة ومرونة ASP.NET MVC .
أصررت أيضًا على الاختبارات التي تعتبر مهمة جدًا في أي تطبيق يحترم نفسه والتي تعتبر عنصرًا مهملاً في كثير من الأحيان.
يمكنك أيضًا المساهمة في تطبيق الخيط الأحمر الرائع ومعرفة كيفية الاستفادة من MVC لتحقيق تطبيق ويب متماسك.
في هذه المرحلة من الدورة التدريبية ، تكون بالفعل قيد التشغيل وقادرًا على إنشاء تطبيقات الويب الخاصة بك. ولكي تكون مقتنعا ، اختبر نفسك في الاختبار الأخير وآخر نشاط في هذه الدورة! يمكنك بعد ذلك تحسين نفسك باستخدام الملاحق في الجزء التالي ، ولكن قبل ذلك ...
... انتقل إلى صفحة الدرس الرئيسية  للقيام بتمارينك!
حظا سعيدا ، لديك النهاية الصحيحة!