ساخت یک برنامه تک صفحه ای با استفاده از Upedia/Jeneva/AngularJS درMVC

این مقاله نشان می دهد که چگونه می توان یک اپلیکیشن تک صفحه ای(SPA) را با استفاده از AngularJS ایجاد کرد. این مقاله برای افرادی که می خواهند در مورد angular و mvvm مطالعه داشته باشند بسیار مفید است. در اینجا از سرویس Jeneva.Net نیز استفاده می کنیم که اعتبار سنجی های معمول در برنامه را به سادگی مدیریت می کند.

ساخت یک برنامه تک صفحه ای با استفاده از Upedia/Jeneva/AngularJS درMVC

پیش زمینه

برای شروع باید از قبل با back-end کار کرده باشید که شامل دو کنترلر است یکی MVC controller که View های HTML را فراهم می کند و دیگری کنترلر WebAPI می باشد، که به ما مربوط به Rest JSON را به صورت دستی api می دهد و به شکل زیر می باشند:

public class ClientController : System.Web.Mvc.Controller
{
    public ActionResult List()
    {
        return this.View();
    }
 
    public ActionResult Create()
    {
        return this.View();
    }
 
    public ActionResult Edit()
    {
        return this.View();
    }
}

 

public class ClientController : System.Web.Http.ApiController
{
    public IClientService ClientService { get; set; }

    public Client GetById(int id)
    {
        return this.ClientService.GetById(id);
    }

    public IList<client> GetAll()
    {
        return this.ClientService.GetAll();
    }

    public void Save(Client item)
    {
        this.ClientService.Save(item);
    }

    public void Update(Client item)
    {
        this.ClientService.Update(item);
    }

    [HttpPost]
    public void Delete(int id)
    {
        this.ClientService.Delete(id);
    }
}</client>

 

هردو کنترلر، ساده و قابل به کارگیری هستند . لطفا به مقاله قبلی مراجعه کرده و مشاهده کنید که چگونه می توان یک  back-end ایجاد کرد. در این مقاله هدف ما ایجاد یک front-end برای این دو کنترلر است. front-end باید یک اپلیکیشن مرورگر تک صفحه ای باشد(SPA). تعدادی از افراد فکر می کنند که SPA تکنیک مفیدی نیست، چرا که لینک های مختلفی برای صفحات متفاوت ایجاد نمی کند به خاطر همین در SPA باید تلاش بیشتری انجام داد تا مطلب مورد نظر را پیدا کرد، که این کار ارزشی ندارد. ولی ما نشان خواهیم داد که چگونه می توان با استفاده از AngularJS  یک SPA به سادگی اپلیکیشن های چند صفحه ای ایجاد کرد، به طوری که تمام مزایای اپلیکیشن های چند صفحه ای در SPA حفظ شده باشد.

ساختار پایه

بخش مهم هر اپلیکیشن تک صفحه ای ساختار آن است. پروژه ما سه View دارد که شامل موارد زیر است:" لیستی از مشتریان"، "افزودن مشتری" و "ویرایش مشتری". این یعنی در واقع سه کنترلر AngularJS نیز برای این View ها داریم که شامل clientListController.js، clientCreateController.js و clientEditController.js می باشد. پوشه vendor شامل تمام کتابخانه های شخص ثالث(third-party) می باشد.

همچنین فیلتر های سفارشی نیز داریم.معمولا این نوع ساختار پوشه های directives و services را نیز دارد، اما در این پروژه SPA ساده، از هیچ نوع directives و services سفارشی استفاده نکرده ایم:

بخش پایه و اساسی front-end فایل app.js می باشد که در ریشه (root) فولدر js می باشد:

var myclients = myclients || {};
myclients.app = angular.module("myclients", ["ngRoute", "jenevamodule"]);
 
myclients.app.config(function ($routeProvider) {
    $routeProvider
        .when("/", {
            templateUrl: "client/list",
            controller: "clientListController"
        })
        .when("/client/list", {
            templateUrl: "client/list",
            controller: "clientListController"
        })
        .when("/client/create", {
            templateUrl: "client/create",
            controller: "clientCreateController"
        })
        .when("/client/edit/:id", {
            templateUrl: "client/edit",
            controller: "clientEditController"
        })
        .otherwise({
            templateUrl: "home/notfound"
        });
});
 
$jeneva.settings.baseUrl = "/api/";

 

در اینجا می خواهیم محتوای کد بالا را مورد تجزیه و تحلیل قرار دهیم.

در خط اول ما فضای نام اصلی اپلیکیشن خود را تعریف می کنیم. سپس با استفاده از AngularJS یک نمونه از ماژول اصلی خود ایجاد می کنیم. این یک کار معمول برای هر نوع اپلیکیشن angular است .  این ماژول به دو ماژول متفاوت دیگر بستگی دارد: ngRoute وjenevamodule که هردو ماژول های مستقل و قابل استفاده مجدد در Angular هستند. جالب ترین بخش در قسمت میانی است. همانطور که می بینید، ما چندین routes در اینجا تعریف کردیم. در واقع این قسمت هسته اصلی هر نوع front-end می باشد. به سادگی می توان گفت که در اینجا کنترلر JS و View باید در دسترس باشند و این به URL داخل مرورگر بستگی دارد. هر لحظه، اپلیکیشن تنها یک View فعال و یک کنترلر فعال دارد و این به URL وارد شده در مرورگر ویندوز بستگی دارد. برای مثال اگر در URL بنویسیم: client/list، پس View مربوط به  client/list و clientListController به صورت فعال بارگذاری خواهد شد.

آخرین بخش از app.js به Jeneva.Net اختصاص دارد. ما به این دلیل از آن استفاده می کنیم که  اپلیکیشن ما را برای توسعه و پشتیبانی راحتتر می سازد. شما می توانید به مقاله قبلی ما مراجعه کرده واطلاعات بیشتری در مورد Jeneva.Net بدست آورید. کنترلر های WebAPI پروژه ما در زیر پوشه (subfolder) با نام /api/ قرار گرفته است.

کنترلرهای JS

حالا اجازه دهید نگاهی به کنترلر ها بیاندازیم. که اولین آنها clientListController.js می باشد:

myclients.app.controller(
    "clientListController", ["$scope", "jeneva",
    function ($scope, jeneva) {
 
    $scope.clientRows = new Array();
 
    $scope.ClientRow = function (id) {
        this.id = id;
        this.name = null;
        this.lastname = null;
        this.age = null;
        this.logins = new Array();
    };
 
    $scope.loadClients = function() {
        jeneva.get("client/getall")
        .then(function (items) {
            angular.forEach(items, function (p, i) {
                var row = new $scope.ClientRow(p.id);
                row.name = p.name;
                row.lastname = p.lastname;
                row.age = p.age;
                angular.forEach(p.logins, function (q, j) {
                    row.logins.push(q.name);
                });
                $scope.clientRows.push(row);
            });
        });
    };

   $scope.onDelete = function (clientId) {
      jeneva.post("client/delete/" + clientId)
      .then(function () {
         $scope.loadClients();
      });
   };
 
    $scope.$on("$routeChangeSuccess", function () {
        jeneva.setScope($scope);
        $scope.loadClients();
    });
}]);

همانطور که می بینید، به ماژول ایجاد شده myclients.app مراجعه می کنیم که دو خط ابتدایی فایل app.js می باشد. کنترلر ما به صورت clientListController نام گذاری شده است و دو متغیر با نام های $scope و jeneva به آن تزریق شده است. در بدنه کنترلر خطوط scope.client$ را تعریف کرده ایم (که مدلی از View ما می باشد) و لیستی از کلاینت های موجود در پایگاه داده می باشد. تعامل با back-end در متد $scope.loadClients انجام می شود که از سرویس jeneva برای فراخوانی کنترلرهای WebAPI استفاده می کند. دلیل این که چرا از سرویس jeneva به جای angular مستقیم (http$) استفاده می کنیم این است که مدیریت jeneva به صورت خودکار فراخوانی می شود، اگر backend ما شکست بخورد پیغام های خطا را در مکان مشخصی از صفحه View نشان می دهد( بر اساس اعتبارسنجی jvPath و دستورات jvErrorkey ). زمانی که WebAPI لیستی از مشتریان را برمی گرداند،  آنها را در فیلد $scope.clientRows منتشر می کنیم.

آخرین بخش کنترلر، مهمترین بخش است. یکhandler به رویداد routeChangeSuccess ضمیمه کرده ایم. این رویداد هر زمانی که کنترلرش فعال شود راه اندازی می شود. برای مثال هر زمان که کاربر به View مربوط به List of Clients (لیست مشتریان) هدایت شود این رویداد راه اندازی می شود. در پروژه ما، زمانی این رویداد راه اندازی می شود، که متد loadClients فراخوانی شود، و داده از backend گرفته می شود. بقیه کنترلر ها نیز به طور مشابه کار می کنند. آنها متغیر  $location در آن تزریق شده که برای هدایت بین View در AngularJS SPA استفاده می شوند. برای مثال، clientCreateController.js:

myclients.app.controller(
    "clientCreateController",
    ["$scope", "$location", "jeneva", function ($scope, $location, jeneva) {
 
    $scope.name = null;
    $scope.lastname = null;
    $scope.age = null;
    $scope.loginRows = new Array();
 
    $scope.LoginRow = function () {
        this.name = null;
        this.password = null;
        this.enabled = false;
    };
 
    $scope.onRemoveLoginClick = function (item) {
        var index = $scope.loginRows.indexOf(item);
        $scope.loginRows.splice(index, 1);
    };
 
    $scope.onAddLoginClick = function () {
        var row = new $scope.LoginRow();
        $scope.loginRows.push(row);
    };
 
    $scope.onSave = function () {
        var data = {};
        data.name = $scope.name;
        data.lastname = $scope.lastname;
        data.age = $scope.age;
        data.logins = new Array();
        angular.forEach($scope.loginRows, function (p, i) {
            var item = {};
            item.name = p.name;
            item.password = p.password;
            item.enabled = p.enabled;
            data.logins.push(item);
        });
        jeneva.post("client/save", data)
        .then(function () {
            $location.path("client/list");
        });
    };
 
    $scope.$on("$routeChangeSuccess", function () {
        $scope.onAddLoginClick();
    });
}]);

نگاهی به متد $scope.onSave بیاندازید. این متد زمانی که کاربر روی دکمه Save از View مربوط به ایجاد مشتری کلیک کند، راه اندازی می شود. این متد تمام داده های user-entered را در یک متغیر داده ای بزرگ جمع آوری می کند و آنها را به صورت JSON و با استفاده از سرویس Jeneva به WebApi ارسال می کند. اگر داده با موفقیت ذخیره شده باشد، متد ( )location.path$ فراخوانی شده و کاربر مجدد به لیست View مربوط به مشتریان هدایت می شود. اگر backend شکست بخورد و یا اعتبار سنجی backend با شکست مواجه شود. سرویس jeneva این مسئله را مدیریت می کند و تمام مشکلات اعتبارسنجی در View را در یک مکان مشخص، بسته به دستورات jeneva نشان می دهد .( جلوتر در این مقاله خواهید دید که چگونه Jeneva این شکست ها را مدیریت می کند).

clientEditController.js نیز به روش مشابه کار می کند، که آن را در کد بالا می بینید.

 

View های HTML

همانطور که در ابتدا گفته شد، تنها سه View وجود دارد: لیستی از کلاینت ها، ایجاد کلاینت و ویرایش کلاینت. اجازه دهید نگاهی به List View بیاندازیم:

<a href="#/client/create">NEW CLIENT</a>
<h2>Clients</h2>
<span class="error" jv-error-key></span>
<hr />
<table border="1" style="width: 50%;">
<thead>
    <tr class="head">
        <th>ID</th>
        <th>NAME</th>
        <th>LASTNAME</th>
        <th>AGE</th>
        <th>LOGINS</th>
        <th></th>
    </tr>
</thead>
<tbody>
    <tr ng-repeat="row in clientRows">
        <td ng-bind="row.id"></td>
        <td ng-bind="row.name"></td>
        <td ng-bind="row.lastname"></td>
        <td ng-bind="row.age"></td>
        <td>
            <div ng-repeat="login in row.logins">
                <span ng-bind="login"></span><br />
            </div>
        </td>
        <td>
            <a ng-href="#/client/edit/{{row.id}}">Edit</a>
        </td>
        <td>
            <input type="button" ng-click="onDelete(row.id)" value="Delete" />
        </td>
    </tr>
</tbody>
</table>

برای کسانی که با AngularJS آشنا هستند، درک این نمونه برنامه بسیار ساده است. این View همراه با clientListController.js می باشد. اساسا این کد به فیلد scope.clientRows$ از کد کنترلرها اشاره می کند و لیستی از کلاینت ها را به صورت جدول HTML نشان می دهد.

جالب ترین قسمت آن لینک زیر است:

<a href="#/client/create">NEW CLIENT</a>

همانطور که می بینید، URL ها با یک علامت # شروع می شوند که در واقع به این معناست که اگر بر روی لینک کلیک کنید، مرورگر صفحه را مجدد بارگذاری نمی کند. در عوض AngularJS این رخداد را ضبط کرده و بر اساس route های ثبت شده در فایل app.js، از یک View فعال به کنترلر فعال دیگر سوییچ می کند. برای مثال اگر شما بر روی لینک NEW CLIENT(افزودن مشتری) کلیک کنید، AngularJS محتوای View جاری را با محتوای View مر بوط به ایجاد کلاینت جایگزین می کند، و کنترلر فعال،  clientCreateController.js خواهد شد.

قسمت جالب دیگر در بالای View قرار دارد:

<span class="error" jv-error-key></span> 

این span توسط کلید jv-error-key  به طور مستقیم از Jeneva کنترل می شود. که در واقع به این معناست، زمانی که پاسخ شکست از سرور backend به مرورگر می رسد، پیغام خطا که یک کلید خالی دارد در بدنه span نشان داده خواهد شد. گاهی اوقات باید از یک پیغام خطا key-less( یا path-less ) استفاده کنیم، برای مثال پیغام Unexpected failure (شکست غیر منتظره) یا تعدادی پیغامهای مشخص دیگر مانند " شما نمی توانید این مشتری را حذف کنید"( شما می توانید تعدادی پیغام خطا بر اساس کلید استاتیک ایجاد کنید). بهترین چیزی که در مورد پیغام شکست key-less یا کلید های استاتیک وجود دارد این است که آنها نیاز به هیچ گونه داده JSOn برای ارسال به سرور ندارند. همچنین علاوه بر پیغام های کلیدی کمتر شما همیشه برای تعریف کلیدهای سفارشی خود آزاد هستید، برای مثال می توانید کلید " server_error" را تعریف کرده سپس شکست ها را نسبت به محتوای اعتبار سنجی register کنید، و بعد از jv-error-key مستقیم برای نمایش پیغام خطا در هرجای فرم که بخواهید استفاده کنید. در این مثال ما از پیغام های key-less برای استثناهای مدیریت نشده استفاده کرده ایم و به طور ساده آنها را در بالای هر فرم نمایش داده ایم. همچنین پیغام های key-less را برای اطلاع به کاربر که نمی تواند یک کلاینت را با توجه به برخی دلایل حذف کند، استفاده کرده ایم(حذف کلاینت را در ClientService مشاهده کنید)

حالا نگاهی به لینک ویرایش مشتری بیاندازید:

<a ng-href="#/client/edit/{{row.id}}">Edit</a> 

همانطور که مشاهد می کنید، این لینک کاربر را به View مربوط به ویرایش کاربر هدایت می کند،  همچنین شامل ID کلاینت نیز هست. یک editClientController باید بداند که چگونه این ID را از لینک استخراج کند. نگاهی به فایل app.js بیاندازید که چگونه یک route برای View مربوط به ویرایش کلاینت تعریف شده است:

.when("/client/edit/:id", {
    templateUrl: "client/edit",
    controller: "clientEditController"
}) 

مکانیسم AngularJS routing قادر به مدیریت پارامترهای URL می باشد و clientEditController می تواند با استفاد از سرویس $routeParams به آنها دستیابی داشته باشد. سرویس routeParams$ باید با روش مشابه به این کنترلر به صورت سرویس های دیگر تزریق شود. کد بالا را برای دیدن جزییات بیشتر مطالعه کنید.

حالا اجازه دهید نگاهی به View ایجاد مشتری بیاندازیم:

<a href="#/client/list">ALL CLIENTS</a>|
<h2>New Client</h2>
<span class="error" jv-error-key></span>
<span class="error" jv-error-key="logins"></span>
<hr />
<table ng-form name="form">
<tr>
    <td>Name</td>
    <td>
        <input name="name" type="text" ng-model="name" jv-path="name" />
        <span class="error" ng-if="form.name.$error.jvpath" ng-repeat="msg in form.name.$jvlist">{{msg}}</span>
    </td>
</tr>
<tr>
    <td>Last name</td>
    <td>
        <input name="lastname" type="text" ng-model="lastname" jv-path="lastname" />
        <span class="error" ng-if="form.lastname.$error.jvpath" ng-repeat="msg in form.lastname.$jvlist">{{msg}}</span>
    </td>
</tr>
<tr>
    <td>Age</td>
    <td>
        <input name="age" type="text" ng-model="age" jv-path="age" />
        <span class="error" ng-if="form.age.$error.jvpath" ng-repeat="msg in form.age.$jvlist">{{msg}}</span>
    </td>
</tr>
<tr>
    <td>Logins</td>
    <td>
        <table>
          <thead>
            <tr>
            <th>Login</th>
            <th>Password</th>
            <th>Enabled</th>
            <th></th>
            </tr>
          </thead>
          <tbody>
            <tr ng-repeat="row in loginRows" ng-form name="loginForm">
            <td>
                <input name="loginName" type="text" ng-model="row.name" jv-path="{{'logins['+ $index + '].name'}}" />
                <span class="error" ng-if="loginForm.loginName.$error.jvpath" ng-repeat="msg in loginForm.loginName.$jvlist">{{msg}}</span>
            </td>
            <td>
                <input name="password" type="text" ng-model="row.password" jv-path="{{'logins['+ $index + '].password'}}" />
                <span class="error" ng-if="loginForm.password.$error.jvpath" ng-repeat="msg in loginForm.password.$jvlist">{{msg}}</span>
            </td>
            <td>
                <input name="enabled" type="checkbox" ng-model="row.enabled" jv-path="{{'logins['+ $index + '].enabled'}}" />
                <span class="error" ng-if="loginForm.enabled.$error.jvpath" ng-repeat="msg in loginForm.enabled.$jvlist">{{msg}}</span>
            </td>
            <td>
                 <input type="button" ng-click="onRemoveLoginClick(row)" value="Delete" />
            </td>
            </tr>
          </tbody>
        </table>
        <div style="padding-bottom: 0.5em;">
             <input type="button" ng-click="onAddLoginClick()" value="Add" />
        </div>
    </td>
</tr>
</table>
<hr />
<input type="button" ng-click="onSave()" value="Save" />

هر المان ورودی به فیلد $scope کنترلرهایی که از Angular به طور مستقیم استفاده کرده اند، گره خورده است. هر المان ورودی توسط پیغام خطا دنبال می شود:

<input name="name" type="text" ng-model="name" jv-path="name" />
<span class="error" ng-if="form.name.$error.jvpath" ng-repeat="msg in form.name.$jvlist">{{msg}}</span>

همانطور که می بینید این span با ng-if و ng-repeat مستقیم مجهز شده است و اساسا به این معناست که اگر اعتبارسنجی برای نام فیلد از loginForm مربوط به span شکست بخورد، پیغام خطا را نمایش خواهد داد. مورد دوم کمی عجیب به نظر می رسد اما این نیز ساده است.  jv-path="{{'logins['+ $index + '].name'}}" به این معناست که اگر فیلد logins[0].name شکست خورده باشد - سپس پیغام خطا در مکان مشخص قرار خواهد گرفت.

View Container

خب ما تا اینجا View ها و کنترلرهایی داشتیم، همچنین  app.js داشتیم که هر چیزی را پیکربندی می کند. مرحله آخر برای ایجاد یک صفحه در single page می باشد. این صفحه تمام View ها و کنترلرها را میزبانی می کند. این صفحه در زمانی که کاربر به ریشه web application هدایت می شود در مرورگر نمایش داده می شود. صفحه container ما در این آدرس قرار داده شده است: views/home/Index.cshtml . و بسیار ساده است:

<!DOCTYPE html>
<html ng-app="myclients">
<head>
   <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
   <title>MyClients Single Page Angular Application</title>
 
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/vendor/angular.js")"></script>
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/vendor/angular-route.js")"></script>
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/vendor/jeneva.angular.js")"></script>
 
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/app.js")"></script>
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/filters/idtext.js")"></script>
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/filters/datetime.js")"></script>
 
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/controllers/clientCreateController.js")"></script>
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/controllers/clientListController.js")"></script>
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/controllers/clientEditController.js")"></script>
   <link href="@Url.Content("~/Resources/css/style.css")" rel="stylesheet"/>
</head>
<body>
    <ng-view>
    </ng-view>
</body>
</html>

اول از همه باید ماژول اپلیکیشن را با استفاده از ng-app directive تعریف کنیم. ماژول ما myclients نام دارد، این ماژول به فایل app.js اشاره می کند. سپس باید شامل angular.js، jeneva.angular.js و تمام کنترلرها directives ها و فیلترها باشد.

المان body شامل فقط یک تگ <ng-view> می باشد که شما از قبل آن را داشته اید، AngularJS محتوای این تگ را با View فعال جاری جایگزین می کند.

با زدن کلید f5 خروجی لیست مشتریان به صورت زیر می شود:

 

ویرایش:

 

 

فایل های ضمیمه
دانلود نسخه ی PDF این مطلب