其實我沒有那麼喜歡寫程式

Validation : The Angular Way(預先定義錯誤訊息)

Angular做驗證的確相當方便,但有一點是非常麻煩的,就是定義錯誤訊息很麻煩(且無聊)。

下面範例有一個姓名欄位為必填欄位,且字數必須大於3個字元和小於10個字元,我們必須幫每個錯誤類型寫一段html的code,如果表單欄位很多肯定很浪費時間。

   <form name="myForm">
       <input name="name" type="text" ng-minlength="3" ng-maxlength="10" required />
       <div ng-show="myForm.name.$error.required">
           請輸入姓名
       </div>
       <div ng-show="myForm.name.$error.minlength">
           至少輸入三個字
       </div>
       <div ng-show="myForm.name.$error.maxlength">
           最多輸入10個字
       </div>
   </form>

定義錯誤訊息的Directive

Angular會給每一個input一個$error的object,我們可以透過key值取得boolean判斷該驗證是否通過,例如$error.required,所以我們利用這個屬性去顯示所有驗證的錯誤訊息。 但我們可以自己定義directive去做這件事,這會節省掉很多打字的時間。 下面範例是定義一個validationErrors的directive,它會注入errorMessages這個constant(我們預先定義好的錯誤訊息)來顯示所有的錯誤訊息。

errorModule
    .directive('validationErrors', function (errorMessages) {
    return {
        restrict: 'A',
        scope: {
            errors: '=',
            errorClass: '@'
        },
        template: '<div ng-class="buildErrorClass()" ng-repeat="(errorKey, isError) in errors track by $index" ng-show="isError">' +
                      '{{errorFor(errorKey)}}' +
                  '</div>',
        controller: function ($scope) {
            $scope.errorFor = function (errorKey) {
                return errorMessages[errorKey];
            };

            $scope.buildErrorClass = function () {
                return this.errorClass || 'inline-help text-error"';
            };
        }
    };
});
errorMessages constant
errorModule
    .constant({
        required: '為必填欄位',
        email: '必須為正確的email格式',
        url: '必須為url格式',
        minlength: '您輸入太少字',
        maxlength: '您輸入太多字了',
        /*
            省略
        */
    });

用Provider取代Constant

但用constant定義錯誤訊息其實彈性不夠,如果你的module希望以後還能用(或是給別人用),它們就必須回去修改你的原始碼。這裡可以改用provider(不要用service或factory,因為我們希望能夠在config block階段定義錯誤訊息,然後提供整個應用程式使用)。

errorMessages provider
errorModule.provider('errorMessages', function () {
    this.errorMessages = {};

    this.$get = function () {
        var errorMessages = this.errorMessages;
        return {
            getError: function () {
                return errorMessages;
            }
        };
    };

    this.setErrorMessages = function (messages) {
        this.errorMessages = messages;
    };

});

接著我們就可以在應用程式的module config注入errorMessagesProvider來定義我們的錯誤訊息,接著我們就可以在應用程式裡使用validationError這個directive了。

myApp module
angular.module('myApp', ['validationError'])
    .config(function(errorMessagesProvider){
        errorMessagesProvider.setErrorMessages({
            required: '為必填欄位',
            email: '必須為正確的email格式',
            url: '必須為url格式',
            minlength: '您輸入太少字',
            maxlength: '您輸入太多字了',
            /*
                省略
            */
        });
    });
});
myApp view
<div ng-app="myApp">
    <form class="form-horizontal" name="logOnForm" novalidate ng-controller="formtrl">
        <input name="userName" type="text" ng-model="userName" required />
        <div errors="logOnForm.userName.$error" error-class="text-danger" validation-errors></div>
        <input name="email" type="email" ng-model="email" required />
        <div errors="logOnForm.email.$error" error-class="text-danger" validation-errors></div>
    </form>
</div>

客製化錯誤訊息

我們已經在config裡定義各種類型的錯誤訊息了,但是有時候你可能希望某些欄位要顯示特定的錯誤訊息。 要做到這一點只需修改directive。

validationErrors directive
errorModule
    .directive('validationErrors', function (errorMessages) {
    return {
        restrict: 'A',
        scope: {
            errors: '=',
            errorClass: '@'
        },
        template: '<div ng-class="buildErrorClass()" ng-repeat="(errorKey, isError) in errors track by $index" ng-show="isError">' +
                      '{{errorFor(errorKey)}}' +
                  '</div>',
        controller: function ($scope, $element, $attrs) {
            var error = errorMessages.getError();
            
            if ($attrs.validationErrors) {
                angular.extend(error, $scope.$eval($attrs.validationErrors));
            };

            $scope.errorFor = function (errorKey) {
                return error[errorKey];
            };

            $scope.buildErrorClass = function () {
                return this.errorClass || 'inline-help text-error"';
            };
        }
    };
})
myApp view
<div ng-app="myApp">
    <form class="form-horizontal" name="logOnForm" novalidate ng-controller="formtrl">
        <input name="userName" type="text" ng-model="userName" required />
        <div errors="logOnForm.userName.$error" 
             error-class="text-danger" 
             validation-errors="{required: '使用者名稱是必填欄位'}">
        </div>
        <input name="email" type="email" ng-model="email" required />
        <div errors="logOnForm.email.$error" 
             error-class="text-danger" 
             validation-errors="{required: 'Email是必填欄位', email: '請輸入正確的Email格式'}">
        </div>
    </form>
</div>

Validation: The Angular Way(自訂驗證Directive)

Angular除了提供HTML5 input type(email、url、number)驗證,也提供一些驗證的directive(ng-pattern、ng-maxlength、ng-minlength、required)。

但這些驗證可能無法滿足你所有的需求,所以你必須自行擴充。 比較偷懶的做法是有些人會直接在controller裡做判斷,這樣不但會讓controller越來越肥,而且你所寫的驗證並不能重複利用,萬一別的地方也需要用到相同規則的驗證的話怎麼辦?

其實擴充驗證的directive沒有想像中的複雜,只要熟悉NgModelController裡的方法就可以幫我們自訂驗證狀態。

NgModelController

在擴充directive前我們必須先稍微了解一下NgModelController的成員

$setValidity(validationErrorKey, isValid)

$setValidity能讓我們設定model的驗證狀態,它的第一個參數為string,代表驗證的類型,第二個參數為bool,表示此驗證是否通過。 $setValidity('required' false)會將required類型的驗證設為invalid。

$formatters

$formatters是一個方法陣列,每當你的model改變時,就會依序執行$formatters裡的所有方法,而每個方法都會接到一個來自上一個方法回傳的參數,最後一個回傳的值將會是view的值(其實還會在執行$render方法)。 這讓你有機會可以改變 Model To View 的值(例如轉成大寫)或改變驗證狀態。

$parsers

$parsers類似$formatters,一樣也是一組方法陣列,不同$formatters的是,它是當view的值改變時(例如更改input的值)的時候執行$parsers所有的方法,最後回傳的值將會設為model的值。這讓你有機會可以改變 View To Model 的值(例如轉成大寫)或改變驗證狀態,不過要注意得是若驗證沒通過建議回傳undefined(你不會希望驗證不過的值還給model,況且angularjs原生的驗證directive也都是這個規則)。

以下範例我們會擴充一個directive,它會驗證所輸入的值是否為整數或浮點數,注意我們使用unshift方法加入$parsers陣列,因為我們想要這個方法可以先被執行(比所有原生的驗證都還要早),所以把它插入在陣列最前面。

script
var app = angular.module('myApp', []);

app.directive('float', function() {
  return {
    require: 'ngModel',
    link: function(scope, elm, attrs, ctrl) {
      var validator = function(value){
        if ( /^\-?\d+(\.\d+)?$/.test(viewValue)) {
          ctrl.$setValidity('float', true);
          return parseFloat(viewValue.replace('.'));
        } else {
          ctrl.$setValidity('float', false);
          return undefined;
        }
      };
    
      ctrl.$parsers.unshift(validator);
      ctrl.$formatters.unshift(validator);
    }
  };
html
<form name="myForm" class="form-horizontal" novalidate ng-submit="logOn()">
    <input name="float" type="text" class="form-control" ng-model="float" float>
    <span ng-show="myForm.float.$error.float && myForm.float.$dirty">
        請輸入浮點數
    </span>
    <input type="submit" ng-disabled="myForm.$invalid" />
</form>

但總會有問題....
請看以下的範例,我在float這個input增加一個required驗證,結果....

html
<form name="myForm" class="form-horizontal" novalidate ng-submit="logOn()">
    <input name="float" type="text" class="form-control" ng-model="float" float required>
    <div ng-show="myForm.float.$error.float && myForm.float.$dirty">
        請輸入浮點數
    </div>
    <div ng-show="myForm.float.$error.required && myForm.float.$dirty">
        必填欄位
    </div>
    <input type="submit" ng-disabled="myForm.$invalid" />
</form>

明明就有輸入值,為何還是顯示必填欄位未填?
那是因為我們將float是用$parsers.unshift()加入$parsers的最前面($formatters也是),所以當float驗證沒過,就會回傳undefined,那接著下來的required驗證方法就會接到undefined,那當然驗證就不會過。 所以如果您希望float在required之後驗證,那就用$parsers.push()

Validation: The Angular Way(基礎)

最近學了一段時間的Angular,也開使用在現有的專案上,剛好有機會可以幫手邊負責的專案擴充一些Angular的驗證directive。不管是學哪個Library和FrameWork,其實驗證一直是我最不感興趣的東西,但最近在幫手邊的專案作用戶端驗證時,發現Angular的驗證真的非常的方便,擴充自己要的驗證directive也不會太難。

這篇文章會先介紹Agular基本的驗證機制,接下來才會進入如何擴充Angularjs驗證的directive。

首先我們先看個範例,有個表單我們希望使用者名稱和Email為必填,且Email必須符合正確的個式。

那Angular如何幫我們驗證呢? 這裡是這個表單的code

<form name="logOnForm" class="form-horizontal" novalidate>
    <input name="userName" type="text" class="form-control" ng-model="userName" required>
    <span ng-show="logOnForm.userName.$error.required">
        請輸入使用者名稱
    </span>
    <input name="email" type="email" class="form-control" id="email" placeholder="Email" ng-model="email" required>
    <span ng-show="logOnForm.email.$error.required">
        請輸入Email
    </span>
</form>

預設能使用的驗證

這裡我沒有寫認何一行javascript的code就做好了基本的用戶端驗證
希望欄位是必填欄位,我只需要在表單欄位加上Html5標準的required屬性,angular就會幫我們驗證該欄位是否合法。那angular提供了哪些基本的驗證呢?

必填

驗證使用者是否輸入值,只要加上簡單的Html5 Attribute required就能夠使用

<input type="text" ng-model="name" required>

最小長度

驗證使用者輸入的文字是否符合最小長度,我們只要加上ng-minlength這個directive

<input type="text" ng-model="name" ng-minlength="3">

最大長度

驗證使用者輸入的文字是否符合最大長度,我們只要加上ng-axlength這個directive

<input type="text" ng-model="name" ng-maxlength="12">

正規表達式

所有angular提供的基本驗證中最有趣的。 我們可以設定正規表達式來進行驗證,使用directive:ng-pattern="/pattern/"

<input type="text" ng-model="name" ng-pattern="/[0-9]/">

數字

必須為數字欄位,只需將type設為number

<input type="number" ng-model="age">

Email

必須為正確的Email,只需將type設為email

<input type="email" ng-model="email">

Url

必須為正確的Url,只需將type設為url

<input type="url" ng-model="url">

表單欄位狀態

那我們要如何知道欄位發生錯誤和定義錯誤訊息呢?
我們可以透過以下的格式取得驗證是否通過
formName.inputFieldName.$error.errorType

例如用logOnForm.userName.$error.required可以取得下面表單的userName欄位是否通過required驗證,若驗證不過則會取得true,通過則取得false

<form name="logOnForm" class="form-horizontal" novalidate role="form" ng-controller="demo1Ctrl">
    <input name="userName" type="text" class="form-control" ng-model="userName" required>
</form>

我們能利用該屬性定義錯誤訊息,若logOnForm.userName.$error.required為true則顯示,若為false則隱藏

<form name="logOnForm" class="form-horizontal" novalidate>
    <input name="userName" type="text" class="form-control" ng-model="userName" required>
    <span ng-show="logOnForm.userName.$error.required">請輸入使用者名稱</span>
</form>
取得其他驗證狀態

formName.inputFieldName.$error.required
formName.inputFieldName.$error.email
formName.inputFieldName.$error.url
formName.inputFieldName.$error.maxlength
formName.inputFieldName.$error.minlength
formName.inputFieldName.$error.pattern

除了$error我們還可以取得其他有用的屬性

$valid

取得該欄位是否通過驗證,若無任何驗證失敗則回傳true,否則回傳false

formName.inputFieldName.$valid;

$invalid

和$valid相反

formName.inputFieldName.$valid;

$pristine

若該input欄位還沒有輸入過值,則為true,否則為false

formName.inputFieldName.$pristine;

$dirty

若該input欄位以經輸入過值,則為true,否則為false

formName.inputFieldName.$dirty;

若我們不希望使用者還沒輸入過表單就出現錯誤訊息,可以用以下技巧

<form name="logOnForm" class="form-horizontal" novalidate>
    <input name="userName" type="text" class="form-control" ng-model="userName" required>
    <span ng-show="logOnForm.userName.$error.required && logOnForm.userName.$dirty">
        請輸入使用者名稱
    </span>
</form>

以上這些屬性form也可以取得
例如 formName.$invalid
所以我們可以利用以下技巧防止使用者送出未通過驗證的表單

<form name="logOnForm" class="form-horizontal" novalidate ng-submit="logOn()">
    <input name="userName" type="text" class="form-control" ng-model="userName" required>
    <span ng-show="logOnForm.userName.$error.required && logOnForm.userName.$dirty">
        請輸入使用者名稱
    </span>
    <input type="submit" ng-disabled="logOnForm.$invalid" />
</form>

或者在Controller裡定義submit事件的方法裡檢查

function formController($scope) {
    $scope.logOn = function(){
        if($scope.logOnForm.$invalid) return;
        //省略
    };
}

為表單欄位設計樣式

AngularJs在處理表單的狀態時,會為表單加入各種狀態的class,每個屬性都有對應的class,我們可以根據這些class定義各種狀態的CSS樣式。
ng-pristine
ng-dirty
ng-valid
ng-invalid
ng-valid-errorType
ng-invalid-errorType

我們可以利用這些class來幫我們的表單增加一些色彩

css
input.ng-invalid{
   background-color:  #fcf8e3;
   border-color: red;
}

input.ng-valid{
   border-color: green;
}

html
<form name="logOnForm" class="form-horizontal" novalidate ng-submit="logOn()">
    <input name="userName" type="text" class="form-control" ng-model="userName" required>
    <span ng-show="logOnForm.userName.$error.required && logOnForm.userName.$dirty">
        請輸入使用者名稱
    </span>
    <input type="submit" ng-disabled="logOnForm.$invalid" />
</form>

Agularjs的驗證真的非常簡單好用,但光會這些很難應付複雜的應用程式,接下來我會介紹如何建立自己的驗證directive和一些我認為好用的小技巧。

自訂 AngularJs Directive (2): Hello World

472097903_6147a13e9d_o.jpg
延續上一篇,這一篇將詳細介紹各個選項的用法,並且會在此篇文章完成簡單的directive

定義Directive的Option說明

屬性名稱 用途
restrict 說明該directive要以何種方式宣告,像是元素、屬性、註解
priority 說明該directive和其他在同個元素的directive的優先度為何,數字越小優先度越高
template 用字串的方式編寫HTML碼,使用該directive的元素將會替換為這裡指定的HTML
templateUrl 同template,但是是指定template的url
replace 若為true則會用template取代原本的Html元素,若為false則將元素insert到元本的Html裡面
transclude 設為true可以將原本的HTML的元素內容移到template定義的HTML元素裡
scope 若給一物件則會建立一個新的scope,若指定為true則繼成自父scope
controller 為directive定義一個controller(大部分的目的是為了和其他directive互動)
require 指定該directive需要和哪些directive互動
link 可以在該方法內增加監聽事件($scope.$watch),或初始化一些資訊
compile 在link執行之前會先被執行,用於用程式動態修改template

Restrict

restrict可以讓你定義directive要用何種方式使用,你可以設為用元素、屬性、註解、class來使用你的directive
下面虛擬碼restirct的值'A'代表這個directive能用屬性的方式在view裡使用。

定義
app.directive('directiveName', function(...){
    return {
        restrict: 'A',
        ..........
    };
});

其他可用的retrict選項

E

表示用元素的方式使用directive
<directiveName></directiveName>

A

表示用屬性的方式使用directive
<div directiveName></div>

C

表示可用class的方式使用directive
<div class='directiveName'></div>

M

表示可用註解的方式使用directive
<!-- directive: directiveName -->

Priority

可以定義這個directive的優先度,若定義directive的元素上有其它的directive,將會決定的它的執行順序
數字越小代表優先度越高,返之

定義
app.directive('directiveName', function(...){
    return {
        priority: 10,
        ..........
    };
});

Template

定義directive最終要呈現在view上的HTML

定義
app.directive('directiveName', function(...){
    return {
        template: '<div>' +
                   '<div>hello</div>' +
                  '</div>'
        ..........
    };
});

TemplateUrl

同Template一樣是定義view要成現的HTML,但是是指定html檔的位置,當你的template有好幾行html要撰寫的時候,用templateUrl取代template會是個好主意...

定義
app.directive('directiveName', function(...){
    return {
        templateUrl: 'myTemplate.html',
        ..........
    };
});

ok! 目前你所學的知識已經可以開發一個簡單的directive了,讓我們來著手開發第一個directive吧

首先先定義我們的directive

js
var app = angular.module('app', []);

app.directive('hello', function(){
    return {
        restrict: 'EA',
        template: '<div>hello world!</div>'
    };
});

接著開始在view使用我們定義的directive
因為我們設定restrict為'EA',代表它可使用元素和屬性的方式使用directive

view
<hello></hello>

結果你會看到網頁輸出 hello world!

很簡單不是嗎?

讓我們在為我們的directive option多加一個屬性replace

js
var app = angular.module('app', []);

app.directive('hello', function(){
    return {
        restrict: 'EA',
        template: '<div>hello world!</div>',
        replace: true
    };
});

輸出的結果一樣為 hello world!,但檢查這兩個範例產生的html你會發現
第一個範例產生的html為

example1
<hello>
   <div>hello world!</div>
</hello>

第二個範例產生的html為

example2
<div>hello world!</div>

看出差別了嗎? 當replace設為true時代表會用template將元本的元素覆蓋掉,若為false(或沒指定),則直接插入至元素的內容裡。

Transclude

若我們想將元素元本的內容移到template裡面的任意位置,該如何做呢?
將transclude設為true,我們可以將元素原本的內容任意的加入到template裡
讓我們在新增一個directive,並將transclude設為true,注意到template,我們在span元素上設定一個ng-transclude的directive,意思是元素原本的內容,將會被新增到裡面。

定義
app.directive('rock', function(){
    return {
        restrict: 'EA',
        template: '<div><span ng-transclude></span> is best band in the world</div>',
        replace: true,
        transclude: true
    };
});
view
<rock>Nirvana</rock>

範例輸出的結果為Nirvana is the best band in the world

仔細觀察它所產出的html如下

html
<div>
    <span ng-transclude>
        <span ng-scope>Nirvana</span>
    </span>
    is the best band in the world
</div>

ok! 我們現在已經完成了兩支簡單的directive,但現實世界不可能這麼簡單...
這些都東西還不足以讓我們寫出有用的directive,下一篇文章將會講解link、compile、scope。

自訂 AngularJs Directive (1) :開始

angular.png

最近一直在學習Angular,因為用慣了jQuery,所以在學Angular時就很好奇有沒有類似 jQuery plugin的功能?
其中最接近jQuery plugin的就是Directive了,它可以像jQuery plugin一樣重複利用,並且讓想要的Html元素賦予行為。

Angular提供了很多很有用的內建指令,通過Directive,能夠讓賦予HTML特殊的行為
舉個例子:
透過ng-repeat這個directive,我們能夠重複Render HTML元素(就像跑回圈一樣)

<ul>
    <li ng-repeat="item in items">{{item.Name}}</li>
</ul>

執行的HTML結果

<ul>
    <li>Lee</li>
    <li>Chen</li>
    <li>Lin</li>
</ul>

因為文章是假設各位已經有寫過Angular了,所以直接切入主題吧!
以下是自訂一個Directive的Pseudocode

var app = angular.module('myApp', []);

app.directive('directiveName', function(injectables){
    var directiveConfig = {
        restrict: string,
        priority: number,
        template: string,
        templateUrl: string,
        replace: bool,
        transclude: bool,
        scope: bool || plain object,
        require: string,
        controller: function controllerName($scope, $element, $attrs, $transclude) {....},
        link: function($scope, $element, $attrs) {....},
        compile: function(telement, tattrs, transclude){
            return function(scope, element, attrs) {...}
        }
    };
    
    return directiveConfig;
});

定義Directive的Option說明

屬性名稱 用途
restrict 說明該directive要以何種方式宣告,像是元素、屬性、註解
priority 說明該directive和其他在同個元素的directive的優先度為何,數字越小優先度越高
template 用字串的方式編寫HTML碼,使用該directive的元素將會替換為這裡指定的HTML
templateUrl 同template,但是是指定template的url
replace 若為true則會用template取代原本的Html元素,若為false則將元素insert到元本的Html裡面
transclude 設為true可以將原本的HTML的元素內容移到template定義的HTML元素裡
scope 若給一物件則會建立一個新的scope,若指定為true則繼成自父scope
controller 為directive定義一個controller(大部分的目的是為了和其他directive互動)
require 指定該directive需要和哪些directive互動
link 可以在該方法內增加監聽事件($scope.$watch),或初始化一些資訊
compile 在link執行之前會先被執行,用於用程式動態修改template

接下來將會詳細介紹這些選項的用途

【ASP.NET MVC】使用MvcSiteMapProvider的DynamicNodeProvider遇到的問題(1)

要實做網站breadcrumbm的功能,用MvcSiteMapProvider可以很方便完成,你可以在SiteMap這檔案定義階層關係

<mvcSiteMapNode title="唱片分類" action="Genre" controller="Store">
    <mvcSiteMapNode title="唱片列表" action="Album"  />
</mvcSiteMapNode>

結果會長的像這樣

首頁 > 唱片分類 > 唱片列表

不過這可能無法滿足很多人的需求,你可能希望唱片分類這節點能依照目前的位置改變名稱
例如

首頁 > Metal > 唱片列表

首頁 > Punk > 唱片列表

這時候就要實做 DynamicNodeProvider
首先你必須先繼承DynamicNodeProviderBase
然後覆寫public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)

CODE:
  • dynamicNode.Title = genre.Name;為要顯示的名稱
  • dynamicNode.RouteValues.Add("genreId",genre.Id)代表遇到該route值顯示該節點
GenreDynamicNodeProvider.cs
public class GenreDynamicNodeProvider:DynamicNodeProviderBase
{
    GenreRepository repository = GetRepository();

    public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
    {
        foreach (var genre in repository.All())
        {
            var dynamicNode = new DynamicNode();
            dynamicNode.Title = genre.Name;
            dynamicNode.RouteValues.Add("genreId",genre.Id);
            yield return dynamicNode;
        }

    }
}
SiteMap:

在SiteMap的部分如下定義
加上dynamicNodeProvider="DynamicProviderNode位置, 組件名稱"

<mvcSiteMapNode title="唱片分類" action="Genre" controller="Store" dynamicNodeProvider="Music.GenreDynamicNodeProvider, Music">
    <mvcSiteMapNode title="唱片列表" action="Album" />
</mvcSiteMapNode>

你可能會想要建立多個階層的動態節點(畢竟音樂類型百百種....)

例如

首頁 > Metal > Black Metal > 唱片列表

首頁 > Metal > Pop Metal > 唱片列表

首頁 > Punk > Emo > 唱片列表

首頁 > Punk > Pop Punk > 唱片列表

你可能和我一樣一開始會這樣寫

Route:
 routes.MapRoute(
     name: "AlbumGenre",
     url: "Home/Genre/{genreId}",
     defaults: new { area = "Manage", controller = "Home", action = "Genre" }
  );
  
 routes.MapRoute(
     name: "AlbumSubGenre",
     url: "Home/Genre/{genreId}/SubGenre/{subGrenreId}",
     defaults: new { area = "Manage", controller = "Home", action = "SubGenre" }
  );
CODE:

記得要為他們的階層關系定義Key和ParentKey,否則他們並不會知道彼此的關係!

主分類
GenreDynamicNodeProvider.cs
public class GenreDynamicNodeProvider:DynamicNodeProviderBase
{
    public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
    {
        GenreRepository repository = GetRepository();
        foreach (var genre in repository.All())
        {
            var dynamicNode = new DynamicNode();
            dynamicNode.Title = genre.Name;
            dynamicNode.RouteValues.Add("genreId",genre.Id);
            dynamicNode.Key="genre_"+genre.Id;
            yield return dynamicNode;
        }

    }
}
次分類
SubGenreDynamicNodeProvider.cs
public class SubGenreDynamicNodeProvider:DynamicNodeProviderBase
{
    public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
    {
        GenreRepository repository = GetRepository();
        foreach (var subGenre in repository.All())
        {
            var dynamicNode = new DynamicNode();
            dynamicNode.Title = subGenre.Name;
            dynamicNode.RouteValues.Add("subGenreId",subGenre.Id);
            dynamicNode.ParentKey="genre_"+subGenre.Genre.Id;
            yield return dynamicNode;
        }

    }
}
SiteMap:
<mvcSiteMapNode title="唱片分類" action="Genre" controller="Store" dynamicNodeProvider="Music.GenreDynamicNodeProvider, Music">
    <mvcSiteMapNode title="唱片次分類" action="SubGenre" controller="Store" dynamicNodeProvider="Music.SubGenreDynamicNodeProvider, Music">
        <mvcSiteMapNode title="唱片列表" action="Album" />
    </mvcSiteMapNode>
</mvcSiteMapNode>

我原本也是這樣寫,不過我在運行網頁的時後發現變得很慢,最後發現原來這種寫法會將每個子節點都複製到父節點上,而不是只將真正的子節點複製給他真正的父親
例如

Metal
  • Black Metal
  • Pop Metal
  • Emo
  • Pop Punk
Punk
  • Black Metal
  • Pop Metal
  • Emo
  • Pop Punk

如果節點數很多的話........就暴了
(在真實世界裡如果你把Emo和Pop Punk歸類到Metal可是會被金屬頭追殺的)
其實我們只需要一個DynamicNodeProvider物件就好了,就像在寫資料結構的LinkList一樣,寫法如下

CODE:
GenreDynamicNodeProvider.cs
public class GenreDynamicNodeProvider:DynamicNodeProviderBase
{
    public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
    {
        GenreRepository repository = GetRepository();
        
        foreach (var genre in repository.All())
        {
            var genreNode = new DynamicNode();
            genreNode.Title = genre.Name;
            genreNode.RouteValues.Add("genreId",genre.Id);
            genreNode.Key="genre_"+genre.Id;
            yield return genreNode;
            
            foreach(var subGenre in genre.SubGenre)
            {
                var subGenreNode = new DynamicNode();
                subGenreNode.Title = subGenre.Name;
                subGenreNode.RouteValues.Add("subGenreId",subGenre.Id);
                subGenreNode.RouteValues.Add("genreId",genre.Id);
                subGenreNode.ParentKey="genre_"+genre.Id;
                yield return subGenreNode;                    
            }
        }

    }
}
SiteMap:
<mvcSiteMapNode title="唱片分類" action="Genre" controller="Store" dynamicNodeProvider="Music.GenreDynamicNodeProvider, Music">
    <mvcSiteMapNode title="唱片列表" action="Album" />
</mvcSiteMapNode>

其實還有一堆問題,過幾天補上>"<

【ASP.NET MVC】Partial View繫結複雜的Mdel遇到的問題

今天在開發上遇到一個問題,我想把傳入到PartialView的Model繫結到Controller,但卻繫結不到值,CODE如下

Model

ArtistViewModel.cs
Public Class ArtistViewModel
{
    public Label Label{get; set;}
    public Artist Artist{get; set;}
}
Artist.cs
Public Class Artist
{
    public int Id {get; set;}
    public string Name{get; set;}
}

View

把Model.Artist傳到PartialView,Artist資料都在ArtistForm這個PartialView裡面填

Create.cshtml
@model ArtistViewModel
@using (Html.BeginForm())
{
    @Html.Partial("ArtistForm",Model.Artist)
    *//其他省略*
    <input type="submit" value="送出" />
}

PartialView如下

ArtistForm.cshtml
@model Artist
@Html.TextBoxFor(model=>model.Name)

Controller

但實際上這樣Controller並繫不到值,mode值為Null

ArtistController.cs
public ActionResult Create(ArtistViewModel model)
{
    return View();
}

原因就是在PartialView下的TextBox產生的Name值為<input type="text" name="Name" />
除非將整個ArtistViewModel傳到PartialView裡面

ArtistForm.cshtml
@model ArtistViewModel
@Html.TextBoxFor(model=>model.Artist.Name)

不過這樣就忘了當初設計Partial View的本意了

不過還好解決這個問題很簡單,只要用EditorTemplate取代PartialView就好,CODE如下

Create.cshtml
@model ArtistViewModel
@using (Html.BeginForm())
{
    @Html.EditorFor(model=>model.Artist)
    *//其他省略*
    <input type="submit" value="送出" />
}