2014年2月27日 星期四

ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- AutoComplete

自動完成 ( AutoComplete ) 在許多網頁上都看得到,假設說一個 Combo 選項資料一多,使用者在找資料時就會花上一些時間,可能選項內容會做排序讓使用者找起來比較方便,但如果資料是中文字串且資料又多的話,那就會加大使用者搜尋難度。

使用自動完成 ( AutoComplete ) 好處就在鍵入關鍵字就能找到完整字詞,且順便可以偵測自己打的字串是否有誤,省時又省力。

接下來我就延續 ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- Combo 連動 來做說明。

數據模式

數據模式有兩個值,local 和 remote,local 是指在頁面載入時只讀一次,remote 是指每一次需要數據時,就會與資料庫要一次資料 ( load store)。
在此使用 local
// 數據模式:本地
queryMode: 'local'

是否允許輸入

ㄧ般來說,文字欄位是不需要這個屬性,xtype: 'combobox' 才需要設置
// 是否允許輸入
editable: true

必須要符合選項

此設置是選項必須是在資料來源內,通常與 editable: true 一起用,若輸入不符合來源資料之一,則取消選取
forceSelection: true

包含搜尋

// 包含字串搜尋
anyMatch: true

未選擇前顯示字串

// 未選擇前顯示字串
emptyText: '請選擇產品類別'

隱藏欄位文字、隱藏下拉按鈕

這樣會看起來像文字欄位
hideLabel: true
hideTrigger: true

列表顯示樣式

listConfig: {
    loadingText: '搜尋中...',
    emptyText: '找不到任何分類',
    // 顯示樣板
    getInnerTpl: function () {
        return '{CategoryID}、{CategoryName}';
    }
}

最小輸入字元出現提示

// 最小輸入字元,此選項控制輸入第 n 個字元則出現提示
minChars: 1


2014年2月24日 星期一

ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- 動態複製表單並送出

在看到這篇教學,請先參考 ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- 送出表單 ( Submit Form )

上一篇教學是教大家如何將產品表單做出來並且送到 Controller 內對資料庫做新增的動作,接下來就來教大家如何做動態複製表單並且送出。

要寫好 Controller 要如何接收:
public HttpResponseMessage Post(IEnumerable<Products> products)
{
    if (ModelState.IsValid)
    {
        foreach(Products _product in products)
            db.Products.Add(_product);
        db.SaveChanges();

        HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.Created, products);
        // response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = products.ProductID }));
        return response;
    }
    else
    {
        return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
    }
}


送到 Controller 必須要注意每一個欄位的 name 都必須是陣列型態的,例如:[0].ProductName、[1].UnitPrice、...等,所以在將表單複製出來後,每個欄位的 name 屬性必須重新命名,這樣一來,Controller 才能接收到正確資訊。

先將表單內容物件宣告出來:
var container = {
    id: 'container',
    height: 250,
    xtype: 'container',
    border: 1,
    padding: '10 10 10 10',
    style: { borderColor: '#000000', borderStyle: 'solid', borderWidth: '1px' },
    defaultType: 'textfield',
    items: [
        {
            itemId: 'ProductNameId',
            name: 'ProductName',
            fieldLabel: '產品名稱',
            maxLength: 50,
            maxLengthText: '最大長度為 50 字元',
            allowBlank: false
        },
        {
            itemId: 'CategoryId',
            name: 'CategoryID',
            fieldLabel: '產品類別',
            xtype: 'combo',
            valueField: 'CategoryID',
            displayField: 'CategoryName',
            queryMode: 'local',
            store: storeCategories,
            emptyText: '請選擇產品類別',
            allowBlank: false
        },
        {
            itemId: 'SupplierId',
            name: 'SupplierID',
            fieldLabel: '貨運公司',
            xtype: 'combo',
            valueField: 'SupplierID',
            displayField: 'CompanyName',
            queryMode: 'local',
            store: storeSuppliers,
            emptyText: '請選擇貨運公司',
            allowBlank: false
        },
        {
            itemId: 'QuantityPerUnitId',
            name: 'QuantityPerUnit',
            fieldLabel: '單位數量',
            maxLength: 50,
            maxLengthText: '最大長度為 50 字元',
            allowBlank: true
        },
        {
            itemId: 'UnitPriceId',
            name: 'UnitPrice',
            fieldLabel: '單價',
            allowBlank: false,
            regex: /^[\$]?[\d]*(.[\d]+)?$/,
            regexText: '金額格式錯誤',
            maskRe: /[\d\$.]/,
        },
        {
            itemId: 'UnitsInStockId',
            name: 'UnitsInStock',
            fieldLabel: '單位庫存量',
            allowBlank: false,
            regex: /^[\d]*$/,
            regexText: '格式錯誤',
            maskRe: /[\d]/,
        },
        {
            itemId: 'UnitsOnOrderId',
            name: 'UnitsOnOrder',
            fieldLabel: '已訂購量',
            allowBlank: false,
            regex: /^[\d]*$/,
            regexText: '格式錯誤',
            maskRe: /[\d]/,
        },
        {
            itemId: 'DiscontinuedId',
            name: 'Discontinued',
            fieldLabel: '不再銷售',
            width: 400,
            xtype: 'radiogroup',
            vertical: false,
            allowBlank: false,
            items: [
                {
                    boxLabel: '是',
                    name: 'discontinued',
                    inputValue: 'true'
                }, {
                    boxLabel: '否',
                    name: 'discontinued',
                    inputValue: 'false'
                }
            ]
        },
    ]
}

而實際上在顯示的表單內的 items 是沒有內容的,如果要預設一筆,就將上面的表單欄位加上去就好了:
var form = Ext.create('Ext.form.Panel', {
    renderTo: Ext.getBody(),
    collapseFirst: false,
    title: '新增產品資料',
    id: 'form',
    bodyPadding: '5 5 5 5',
    width: 500,
    fieldDefaults: {
        msgTarget: 'side',
        labelWidth: 120
    },
    defaults: {
        margin: '0 0 5 0',
    },
    plugins: {
        ptype: 'datatip'
    },
    defaultType: 'textfield',
    items: [
        
    ],
    tools: [
       {
           type: 'save',
           callback: function () {
               // show help here
               if (this.up('form').getForm().isValid()) {
                   $.ajax({
                       url: 'http://localhost:8090/api/Product/Post',
                       data: this.up('form').getForm().getValues(),
                       type: 'POST',
                       error: function () { alert('Error!!'); },
                       success: function () {
                           alert('Success!!');
                       }
                   });
               }
               console.log(this.up('form').getForm().getValues())
           }
       },
       {
           type: 'plus',
           callback: function () {
               // show help here
               newContainer();

           }
       }
    ],
});


利用 jQuery.extend 將表單複製出來,要另外寫 function 做這件事情,在 function 必須要做到複製以及將 name 更名,比較要注意的是,:
var counterNew = 0; 
function newContainer() {
    var tContainer = jQuery.extend(true, {}, container);
    var tHeader = '[' + counterNew + '].';
    tContainer.id = tContainer.id + counterNew;
    for (var i = 0; i < tContainer.items.length; i++) {
        
        console.log(tContainer.items[i].xtype);

        if (tContainer.items[i].xtype == 'radiogroup')
        {
            for (var j = 0 ; j < tContainer.items[i].items.length; j++) {
                tContainer.items[i].items[j].name = tHeader + tContainer.items[i].items[j].name;
            }
        }
        else {
            tContainer.items[i].name = tHeader + tContainer.items[i].name;
        }
    }
    Ext.getCmp('form').add(tContainer);
    counterNew++;
}

最後要在實際表單上加上 tools,按下時呼叫此 function:
{
    type: 'plus',
    callback: function () {
        // show help here
        newContainer();
    }
}

頁面上就會顯示:

執行後按下新增按鈕,如果不打上任何資料,會如同以下圖:

打上資料後送出:

程式中設定中斷點,就會看到資料皆有被傳遞:

最後資料庫就會新增此筆資料:


2014年2月21日 星期五

ASP.NET MVC 4 Web Api 回傳HttpResponseMessage遇到 System.ArgumentNullException: Value cannot be null. Parameter name: request

背景為:
1.Client呼叫WebApi
2.WebApi Controller繼承主要Class
3.主要Class繼承ApiController

這次Controller的某個Action使用要其他Class的Method,並接收執行結果(HttpResponseMessage)

Controller Code:
public HttpResponseMessage CheckId(string id)
{
    ClassEmployee Employee = new ClassEmployee();
    HttpResponseMessage response = Employee.Chk(id);
    return response;
}

Class Code:
public class Employee
{
    public HttpResponseMessage Chk(string id)
    {
        //code...
        if(HasEmployee)
        {
            return Request.CreateResponse(HttpStatusCode.OK);
        }
        else
        {
            return Request.CreateResponse(HttpStatusCode.NotFound);
        }
    }
}

在繼承的時候都很正常,而C#又不支援多重繼承,用建立物件的方式去呼叫Method,就出現了下面的錯誤:

ExceptionMessage: 值不能為 null。參數名稱: request

ExceptionType: System.ArgumentNullException

StackTrace:於 System.Net.Http.HttpRequestMessageExtensions.CreateResponse(HttpRequestMessage request, HttpStatusCode statusCode)


原來是因為建立物件的時候,呼叫Controller的Request並不會傳進去,所以Class的Request是null
在建立物件的時候我們要先初始化Class的Request
Controller Code:
public HttpResponseMessage CheckId(string id)
{
    ClassEmployee Employee = new ClassEmployee()
    {
        Request = new HttpResponseMessage()
    }
    HttpResponseMessage response = Employee.Chk(id);
    return response;
}

以上就可以正常執行


MVC4 TDD-System.ArgumentNullException: 值不能为空。
Unit testing a ASP.NET WebAPI controller

ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- 送出表單 ( Submit Form )

在看到這篇教學,請先參考 Visual Studio 2012 安裝 Northwind 資料庫並建立 Entity Framework Database First ( .edmx ) 以及 ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- 基本配置ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- 基本配置

在此利用北風資料庫 ( Northwind Database ) 做一個新增產品 ( Product ) 的頁面。

建立 Controller

注意下拉選單必須要和 Ext.data.JsonStore 資料結合。

先做下拉式選單資料來源的 Controller:
public IEnumerable<Suppliers> Get()
{
    return db.Suppliers.AsEnumerable();
}

...

public IEnumerable<Categories> Get()
{
    var categories = db.Categories.Include(x => x.Products);

    return db.Categories.AsEnumerable();
}

建立資料模型與內容


再來在 View 端宣告下拉是選單的資料模型以及資料內容 ( Category、Supplier ):
var storeCategories = Ext.create('Ext.data.JsonStore', {
    storeId: 'categories',
    fields: ['CategoryID', 'CategoryName'],
    proxy: {
        type: 'ajax',
        url: 'http://localhost/api/Categories/',
        reader: {
            type: 'json',
        }
    },
    autoLoad: true,
});

var storeSuppliers = Ext.create('Ext.data.JsonStore', {
    storeId: 'suppliers',
    fields: ['SupplierID', 'CompanyName'],
    proxy: {
        type: 'ajax',
        url: 'http://localhost/api/Suppliers/',
        reader: {
            type: 'json',
        }
    },
    autoLoad: true,
});

建立產品表單


接著就要做表單 ( form ),要加上驗證的話請參考ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- 欄位驗證 ( Validation )
var form = Ext.create('Ext.form.Panel', {
    renderTo: Ext.getBody(),
    collapseFirst: false,
    title: '新增產品資料',
    id: 'form',
    bodyPadding: '5 5 5 5',
    width: 500,
    fieldDefaults: {
        msgTarget: 'side',
        labelWidth: 120
    },
    defaults: {
        margin: '0 0 5 0',
    },
    plugins: {
        ptype: 'datatip'
    },
    defaultType: 'textfield',
    items: [
        {
            itemId: 'ProductNameId',
            name: 'ProductName',
            fieldLabel: '產品名稱',
            maxLength: 50,
            maxLengthText: '最大長度為 50 字元',
            allowBlank: false
        },
        {
            itemId: 'CategoryId',
            name: 'CategoryID',
            fieldLabel: '產品類別',
            xtype: 'combo',
            valueField: 'CategoryID',
            displayField: 'CategoryName',
            queryMode: 'local',
            store: storeCategories,
            emptyText: '請選擇產品類別',
            allowBlank: false
        },
        {
            itemId: 'SupplierId',
            name: 'SupplierID',
            fieldLabel: '貨運公司',
            xtype: 'combo',
            valueField: 'SupplierID',
            displayField: 'CompanyName',
            queryMode: 'local',
            store: storeSuppliers,
            emptyText: '請選擇貨運公司',
            allowBlank: false
        },
        {
            itemId: 'QuantityPerUnitId',
            name: 'QuantityPerUnit',
            fieldLabel: '單位數量',
            maxLength: 50,
            maxLengthText: '最大長度為 50 字元',
            allowBlank: true
        },
        {
            itemId: 'UnitPriceId',
            name: 'UnitPrice',
            fieldLabel: '單價',
            allowBlank: false,
            regex: /^[\$]?[\d]*(.[\d]+)?$/,
            regexText: '金額格式錯誤',
            maskRe: /[\d\$.]/,
        },
        {
            itemId: 'UnitsInStockId',
            name: 'UnitsInStock',
            fieldLabel: '單位庫存量',
            allowBlank: false,
            regex: /^[\d]*$/,
            regexText: '格式錯誤',
            maskRe: /[\d]/,
        },
        {
            itemId: 'UnitsOnOrderId',
            name: 'UnitsOnOrder',
            fieldLabel: '已訂購量',
            allowBlank: false,
            regex: /^[\d]*$/,
            regexText: '格式錯誤',
            maskRe: /[\d]/,
        },
        {
            itemId: 'DiscontinuedId',
            name: 'Discontinued',
            fieldLabel: '不再銷售',
            width: 400,
            xtype: 'radiogroup',
            vertical: false,
            allowBlank: false,
            items: [
                {
                    boxLabel: '是',
                    name: 'discontinued',
                    inputValue: 'true'
                }, {
                    boxLabel: '否',
                    name: 'discontinued',
                    inputValue: 'false'
                }
            ]
        },
    ],
    tools: [
       {
           type: 'save',
           // hidden: true,
           callback: function () {
               if (this.up('form').getForm().isValid()) {
                   $.ajax({
                       url: 'http://localhost:8090/api/Product/Post',
                       data: this.up('form').getForm().getValues(),
                       type: 'POST',
                       error: function () { alert('Error!!'); },
                       success: function () {
                           alert('Success!!');
                       }
                   });
               }
               console.log(this.up('form').getForm().getValues());
           }
       }
    ]
});

執行後如果不打上任何資料,會如同以下圖:

打上資料後送出:

程式中設定中斷點,就會看到資料皆有被傳遞:

最後資料庫就會新增此筆資料:


2014年2月18日 星期二

ASP.NET MVC 4 WebApi 遇到 「LINQ to Entities 無法辨識方法 '函式名稱(Function_Name)' 方法,而且這個方法無法轉譯成存放區運算式 」 解決方法

在做資料讀取時,有 CustomerID 要撈出所有訂購的項目,所以要使用 CustomerID 篩選出 Orders 資料後再去與 Order Details 做篩選,最後就會找出所有訂購產品的項目,資料表關聯如下:


接著在 Controller 這樣使用:
List<Orders> orders = db.Orders
    .Where(x => x.Customers.CustomerID == id)
    .ToList();

List<Order_Details> order_details = db.Order_Details
    .Include(x => x.Orders)
    .Include(x => x.Products)
    .Where(x => orders.Exists(p => x.Orders.OrderID == p.OrderID))
    .ToList();

就會發生「LINQ to Entities 無法辨識方法 'Boolean Exists(System.Predicate`1[MVVMwithExtjs.Models.Orders])' 方法,而且這個方法無法轉譯成存放區運算式。

後來發現,LINQ to SQL 時無法產生相同的 function,除非你在 SQL Server 上建立相同的預存程序,但是 Where 內又必須使用 LINQ Expression 篩選,可以參考 當EF LINQ Expression遇上Methods如何於LINQ to Entities中使用SQL Server的使用者自訂函數,可能可以解決這個問題,但是都頗麻煩的,所以後來把這資料先實例化出來再做篩選:
List<Orders> orders = db.Orders
    .Where(x => x.Customers.CustomerID == id)
    .ToList();

List<Order_Details> order_details = db.Order_Details
    .Include(x => x.Orders)
    .Include(x => x.Products)
    .ToList()
    .Where(x => orders.Exists(p => x.Orders.OrderID == p.OrderID))
    .ToList();


2014年2月16日 星期日

ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- 工具提示 ( ToolTip )

工具提示一般使用在控制項上,並描述此控制項的用途,對於用戶來說,會使得介面更友善,且不會占用網頁版面,可以說是一舉兩得的功能。

接下來我們就針對 Button 來做 ToolTip 的介紹。

buttons: [{
    text: 'Send',
    id: 'buttonId',
    // button 按下動作
    handler: function () {

    }
}, {
    text: 'Cancel'
}]
執行後如圖示

基本設定

new Ext.ToolTip({
    // 目標 ID
    target: 'buttonId',
    html: '按下儲存表單'
});
執行後如圖示

自動顯示、關閉按鈕、拖曳

new Ext.ToolTip({
    // 目標 ID
    target: 'buttonId',
    html: '按下按鈕儲存表單',
    // 是否自動隱藏,設定 true 會過一段時間會隱藏
    autoHide: false, 
    // 顯示關閉按鈕
    closable: true,
    // 可否拖拉
    draggable: true
});
執行後如圖示

最大寬度、最小寬度

new Ext.ToolTip({
    // 目標 ID
    target: 'buttonId',
    html: '按下按鈕儲存表單',
    maxWidth: 100
});
執行後如圖示

new Ext.ToolTip({
    // 目標 ID
    target: 'buttonId',
    html: '按下按鈕儲存表單',
    minWidth: 200
});
執行後如圖示

跟隨滑鼠、延遲顯示、延遲隱藏、顯示時間


new Ext.ToolTip({
    // 目標 ID
    target: 'buttonId',
    html: '按下按鈕儲存表單',
    // 跟隨滑鼠移動
    trackMouse: true,
    // 延遲顯示 ( 毫秒 )
    showDelay: 1000,
    // 延遲隱藏 ( true & false ) <- 此功能我還不知道怎麼使用 
    hideDelay: false,
    // 顯示時間 ( 毫秒 )
    dismissDelay: 1000
});

從 API 來的提示

建立一個 API
[HttpGet]
public string SaveToolTip(int id)
{
    string username = string.Empty;
    if (id == 1)
    {
        username = "David Kuo";
    }

    return "儲存 " + username + " 的資料";

}
而 View 端如以下設定:
new Ext.ToolTip({
    // 目標 ID
    target: 'buttonId',
    html: '按下按鈕儲存表單',
    autoLoad: {
        url: 'http://localhost:8090/api/Values/SaveToolTip',
        params: { id: "1" },
        method: 'GET'
    },

});
執行後如圖示

指定提示位置

new Ext.ToolTip({
    // 目標 ID
    target: 'buttonId',
    html: '按下按鈕儲存表單',
    // top ( 上 )、bottom ( 下 ﹞、right ( 右 )、left ( 左 )
    anchor: 'top',
    // 寬度
    width: 120,
    // 箭頭位置
    anchorOffset: 30,

});

大致上工具提示 ( ToolTip ) 的用法在上面都找得到,如果各位還有甚麼想法可以在底下分享,我會再加上去的。



2014年2月13日 星期四

ASP.NET MVC 4 WebApi 使用 IE 檢視彈出另存視窗的解決方法

有時候要用 IE 檢視 WebApi 都會彈出另存視窗,都需要先存取 json 檔案才能檢視其 API 之內容


一開始覺得這不會很麻煩,但時間一長就會覺得很麻煩,這時候就會想說有沒有更好的瀏覽器來替代 IE,此時,google chorme 就顯得強大許多了,同樣的東西,檢視內容卻完全不一樣:


但是,不能就這樣擺著讓 IE 一直這樣,所以後來上網搜尋一下,找到解決方案,請照以下設定來解決

Regedit

由於在 IE 的 MIME 中找不到 application/json 格式,IE 會認為它是需要下載的檔案,才會彈出另存視窗,所以必須要在登錄檔中加上去。

開啟一個記事本,內容為:
Windows Registry Editor Version 5.00
;
; Tell IE to open JSON documents in the browser.  
; 25336920-03F9-11cf-8FD0-00AA00686F13 is the CLSID for the "Browse in place" .
;  

[HKEY_CLASSES_ROOT\MIME\Database\Content Type\application/json]
"CLSID"="{25336920-03F9-11cf-8FD0-00AA00686F13}"
"Encoding"=hex:08,00,00,00

[HKEY_CLASSES_ROOT\MIME\Database\Content Type\text/json]
"CLSID"="{25336920-03F9-11cf-8FD0-00AA00686F13}"
"Encoding"=hex:08,00,00,00

另存成 json-ie.reg 檔案,並且雙擊執行。

這個動作是要讓 IE 能夠在讀取到 application/json、text/json 格式時檢視就好,不需要彈出另存視窗儲存。

所以最後重開 IE 在讀取 json 檔,就會如圖示:



2014年2月10日 星期一

ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- 欄位驗證 ( Validation )

說到 ASP.NET MVC 4 欄位驗證,一想到就是直接在 Model 做設定,View 端就可以直接驗證資料,並且為了防止有人將資料或者不透過網頁直接送到 Controller,在 Controller 也有 ModelState.IsValid 可以檢查資料是否正確。

但是在 MVC 4 WebApi 和 Extjs 中,Controller 可以照原本的方式處理,但是 View 端就必須要使用 Extjs 的驗證了。

以下介紹 Extjs 驗證的設定方法解說:

最大長度

maxLength: 20
// 錯誤訊息
maxLengthText: '最大長度為 20 字元' 


最小長度

minLength: 5
// 錯誤訊息
minLengthText: '最小長度為 5 字元'


是否空值

// 允許為空
allowBlank: false
// 錯誤訊息
blankText: '此欄必填'


Regex

// regex 驗證
regex: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
// 錯誤訊息
regexText: '請輸入 IP 格式'
// 輸入遮罩,意旨只能輸入以下格式文字在欄位內,其他格式會擋住。
maskRe: /[\d\.]/i


vtype


預設有四個型態:「alpha」( 字母 )、「alphanum」( 字母數字 )、「email」( 電子郵件 )、「url」( 網址 )。

vtype: 'email'
// 錯誤訊息
vtypeText: '不符合 Email 格式'


自訂 vtype


自訂一個 vtype 就要使用 Ext.apply 將 Ext.form.field.VTypes 複製出來,加上自訂的驗證,在欄位上使用即可。

參考範本:
Ext.apply(Ext.form.field.VTypes, {
    XXX: function (v) {
        return /^ regex pattern $/.test(v);
    },
    // 錯誤訊息
    XXXText: 'error message',
    // 輸入遮罩,意旨只能輸入以下格式文字在欄位內,其他格式會擋住。
    XXXMask: / regex pattern /i
});

在欄位上使用就加上 vtype: 'XXX' 就可以使用了。

例如:
Ext.apply(Ext.form.field.VTypes, {
    IPAddress: function (v) {
        return /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(v);
    },
    IPAddressText: '請輸入 IP 格式',
    IPAddressMask: /[\d\.]/i
});

欄位中使用:
vtype: 'IPAddress'

效果其實與 Regex 欄位驗證一樣,但是這種方法可以集中管理、重複使用,比較建議使用這種做法。

更多的 驗證方法 於 「More VTypes!」 可以找到參考。

進階用法:ExtJS中表單應驗使用自定義vtype示例


完整程式碼:
//載入會用到的程式
Ext.require([
    'Ext.form.*',
    'Ext.layout.container.Absolute',
    'Ext.window.Window',

    'Ext.Button'
]);

Ext.apply(Ext.form.field.VTypes, {
    IPAddress: function (v) {
        return /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(v);
    },
    IPAddressText: '請輸入 IP 格式',
    IPAddressMask: /[\d\.]/i
});

Ext.onReady(function () {
    //建立 form 框架
    var form = Ext.create('Ext.form.Panel', {
        renderTo: Ext.getBody(),
        defaultType: 'textfield', 
        border: false, 
        layout: 'form',
        padding: 10,
        items:
        [
            {
                id: 'textfieldId', 
                xtype: 'textfield', 
                fieldLabel: '文字欄位', 
                msgTarget: 'side', 
                name: 'textfield',
                //最大長度
                maxLength: 20,
                maxLengthText: '最大長度為 20 字元',
                // 最小長度
                minLength: 5,
                minLengthText: '最小長度為 5 字元',
                // 允許為空 false 
                allowBlank: false,
                blankText: '此欄必填',
                // alpha , alphanum , email , url
                vtype: 'IPAddress',
                vtypeText: '不符合 Email 格式',
                // regex 驗證
                regex: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
                // 錯誤訊息
                regexText: '請輸入 IP 格式',
                // 輸入遮罩,意旨只能輸入以下格式文字在欄位內,其他格式會擋住。
                maskRe: /[\d\.]/i
            }
        ],
        buttons: [{
            text: 'Send',
            // button 按下動作
            handler: function () {
                // 檢查form isValid  檢查條件是否符合
                this.up('form').getForm().isValid();
                
            }
        }, {
            text: 'Cancel'
        }]
    });

});




2014年2月7日 星期五

ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- 上傳檔案 ( FileUpload )

檔案上傳在 ASP.NET Web Form 中其實只要拉控制項就好,甚至可以做很美觀的介面顯得更友善。

一般 ASP.NET MVC 4 上傳檔案可以參考:ASP.NET MVC - 檔案上傳的基本操作,而使用 ASP.NET MVC 4 WebApi 結合 Extjs 上傳檔案,可以參考以下做法:

View

使用 Extjs 建立一個 file 欄位 ( 備註:allowBlank : false 為必填 )

{
    xtype: 'filefield',
    name: 'photo',
    fieldLabel: 'Photo',
    labelWidth: 100,
    msgTarget: 'side',
    allowBlank: false,
    anchor: '100%',
    buttonText: 'Select Photo...'
}

畫面如下:

Button

建立一個按鈕對應 Api 來上傳檔案:

buttons: [{
    text: 'Send',
    // button 按下動作
    handler: function () {
        // 檢查 form isValid 檢查條件是否符合 加if 成為判斷式
        this.up('form').getForm().isValid();
        // 傳送檔案
        this.up('form').getForm().submit({
            // 傳送到 url
            url: 'http://localhost/api/Upload',
            waitMsg: 'Uploading your photo...',
            success: function (fp, o) {
                Ext.Msg.alert('Success', 'Your photo "' + o.result.file + '" has been uploaded.');
            }
        });
    }
}, {
    text: 'Cancel'
}]


Fakepath 解決方案

當開始測試上傳時,發現檔案上傳路徑都不是真實路徑,而會是 C:\fakepath\ 開頭的路徑,實際找尋這個路徑,發現根本不存在。


問題來了,現在幾乎每個瀏覽器都會將實際路徑隱藏,這是為了安全性著想,所以將實際路徑以 C:\fakepath\ 取代。

這個問題,其實本是用戶端安全性上考量不提供實際路徑,所以沒有辦法找到檔案上傳,但是,我們不能把這些問題都推給用戶去處理,因為他或許不知道問題在哪,而是我們程式人員必須要解決這問題。

後來發現,在 Controller 可以解決這個問題。

Controller

在 Request.Content 內,可以使用 Request.Content.IsMimeMultipartContent() 判斷是否 MIME 多組件內容,再將 Multipart 內檔案讀到 MemoryStream,最後轉型成 Byte[],再存成實體檔案。

[HttpPost]
public HttpResponseMessage Post()
{
    if (Request.Content.IsMimeMultipartContent())
    {
        var streamProvider = new MultipartMemoryStreamProvider();
        Request.Content.ReadAsMultipartAsync(streamProvider).ContinueWith(t =>
        {
            foreach (var item in streamProvider.Contents)
            {
                if (item.Headers.ContentDisposition.FileName == null)
                {
                    // 參數區
                }
                else
                {
                    System.IO.Stream stream = item.ReadAsStreamAsync().Result;

                    byte[] bytes = new byte[stream.Length];
                    stream.Read(bytes, 0, bytes.Length);

                    System.IO.File.WriteAllBytes(@"D:\Image\" + item.Headers.ContentDisposition.FileName.Replace(@"""", @""), bytes);
                }
            }
        });
    }
    return Request.CreateResponse(HttpStatusCode.OK);
}

所以這不管是在是否有實際路徑的情況下都能適用,用戶端不管是否停用或啟用安全性,都會透過 Multipart 傳送檔案到 MemoryStream,在 Controller 讀取這些檔案並且存成實體檔案。


2014年2月5日 星期三

ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- Combo 連動

兩個或更多的下拉式選項要做資料連動,前提就是要在這兩份資料必須要有關聯,不管是一對多還是多對一。

以往在做 WebForm 的時候,如果碰到要下拉選單連動的時候,都要在每次選項切換時,再去和資料庫撈一次資料,如果伺服器是自己的那倒無所謂,如果是使用雲端,使用多少算多少的計費方式,就不得不去計較小細節,因為這小細節很可能會變成大費用。

最理想的情況下,就是將連動的資料在 API 內就先做好,然後在頁面載入時,跟 API 要資料,也只要這麼一次,往後不管怎麼變動,都與資料庫無關。

所以現在就使用 Extjs 來實現 Combo 連動:

在此之前,若未建立資料庫和基本配置,請先參考 Visual Studio 2012 安裝 Northwind 資料庫並建立 Entity Framework Database First ( .edmx ) 以及 ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- 基本配置

建立 Controller API

使用 Northwind 資料庫內產品類別和產品的一對多關聯的特性,來產生資料:
public class CategoriesController : BaseApiController
{
    public IEnumerable<Categories> Get()
    {
        var categories = db.Categories.Include(x => x.Products);

        foreach (Categories category in categories)
        {
            foreach (Products product in category.Products)
                product.Categories = null;
        }

        return db.Categories.AsEnumerable();
    }

    protected override void Dispose(bool disposing)
    {
        db.Dispose();
        base.Dispose(disposing);
    }
}

資料格式為:

建立 Ext Model

必須要注意兩個屬性 hasMany 和 belongsTo,記得要對應好,hasMany 可以想成是這些資料要去連動哪個 Model,belongsTo 資料要依據哪一個 Model 來產生。來看程式碼就會知道了:
Ext.define('Category', {
    extend: 'Ext.data.Model',
    fields: [
        'CategoryID', 'CategoryName'
    ],
    hasMany: { model: 'Product', name: 'Products' }
})

Ext.define('Product', {
    extend: 'Ext.data.Model',
    fields: [
        'ProductID', 'ProductName'
    ],
    belongsTo: 'Category',
})

接著將資料放到 Ext.data.JsonStore 物件內,請注意:產品資料內容不需要建立
var storeCategory = Ext.create('Ext.data.JsonStore', {
    storeId: 'storeCategory',
    model: 'Category',
    proxy: {
        type: 'ajax',
        url: 'http://localhost/api/Categories/',
        reader: {
            type: 'json',
        }
    },
    // 自動載入
    autoLoad: true, 
    listeners: {
        // sotre load 完成後載入這個fucntion  
        load: function (store, records, options) {
            
        }
    },
})

var storeProduct = Ext.create('Ext.data.JsonStore', {
    storeId: 'storeProduct',
    model: 'Product',
})

建立欄位

這裡與上面的 Ext.data.JsonStore 有關聯,為什麼不建立產品資料內容?就是要等選擇後再建立。

combobox 內有監看 ( listeners ) 事件,就等於 WebForm 的事件一樣,選擇後,先將產品下拉式選單清空,再將此資料下的 Product 資料放進去。

[
    {
        xtype: 'combobox',
        fieldLabel: '類別',
        name: 'Category',
        //queryMode: 'local',
        store: storeCategory,
        editable:false,
        valueField: 'CategoryID',
        displayField: 'CategoryName',
        listeners: {
            select: function (combo, record, index) {
                // raw 取得選取的資料
                var tempProducts = record[0].raw.Products; 
                // 清空 product combobox 的store
                storeProduct.removeAll();
                for (var i = 0; i < tempProducts.length; i++) {
                    // 加到 product combobox 的store
                    storeProduct.add(tempProducts[i]); 
                }
            }
        }
    },
    {
        xtype: 'combobox',
        fieldLabel: '產品',
        queryMode: 'local',
        name: 'Product',
        editable: false,
        store: storeProduct,
        valueField: 'ProductID',
        displayField: 'ProductName',
        
    }
]

最後結果就會如同以下畫面:

完整程式碼為:
// 載入會用到的程式
Ext.require([
    'Ext.form.*',
    'Ext.layout.container.Absolute',
    'Ext.window.Window',

    'Ext.Button'
]);

Ext.onReady(function () {
Ext.define('Category', {
    extend: 'Ext.data.Model',
    fields: [
        'CategoryID', 'CategoryName'
    ],
    // Model Category 有 list Product 
    hasMany: { model: 'Product', name: 'Products' }
})

Ext.define('Product', {
    extend: 'Ext.data.Model',
    fields: [
        'ProductID', 'ProductName'
    ],
    belongsTo: 'Category',
})

var storeCategory = Ext.create('Ext.data.JsonStore', {
    storeId: 'storeCategory',
    model: 'Category',
    proxy: {
        type: 'ajax',
        url: 'http://localhost:8090/api/Categories/',
        reader: {
            type: 'json',
        }
    },
    // 自動載入
    autoLoad: true, 
    listeners: {
        // sotre load 完成後載入這個fucntion  
        load: function (store, records, options) {
            
        }
    },
})

var storeProduct = Ext.create('Ext.data.JsonStore', {
    storeId: 'storeProduct',
    model: 'Product',
})

    //建立 form 框架
    var form = Ext.create('Ext.form.Panel', {
        // 預設 tpe (xtype) 沒有指定就是textfield
        defaultType: 'textfield', 
        // 不要外框
        border: false, 
        width: 300,
        // form 的形式顯示 items 自動填滿畫面
        layout: 'form', 
        // css padding 10 px
        padding: 10, 
        renderTo: Ext.getBody(),
        items:
        [
            {
                xtype: 'combobox',
                fieldLabel: '類別',
                name: 'Category',
                //queryMode: 'local',
                store: storeCategory,
                editable:false,
                valueField: 'CategoryID',
                displayField: 'CategoryName',
                listeners: {
                    select: function (combo, record, index) {
                        // raw 取得選取的資料
                        var tempProducts = record[0].raw.Products; 
                        // 清空 product combobox 的store
                        storeProduct.removeAll();
                        for (var i = 0; i < tempProducts.length; i++) {
                            // 加到 product combobox 的store
                            storeProduct.add(tempProducts[i]); 
                        }
                    }
                }
            },
            {
                xtype: 'combobox',
                fieldLabel: '產品',
                queryMode: 'local',
                name: 'Product',
                editable: false,
                store: storeProduct,
                valueField: 'ProductID',
                displayField: 'ProductName',
                
            },
            
        ],
        buttons: [{
            text: 'Send',
            // button 按下動作
            handler: function () {
                // 檢查form isValid  檢查條件是否符合
                this.up('form').getForm().isValid();
                // 取得from 所有欄位與值 name : value
                alert(JSON.stringify(this.up('form').getForm().getValues())) 
            }
        }, {
            text: 'Cancel'
        }]
    });

});


2014年2月3日 星期一

ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- 日期欄位

一般使用日期選擇器 ( date picker ) 如果不支援 HTML5 就可能會考慮使用 Datepicker | jQuery UI 來設定,如果支援 HTML5 的話就只需要在 HTML 內設定就好了:

<input type="date" name="bday">


但 HTML 5 的瀏覽器應該在最新版都有支援,如果用戶端有較舊的瀏覽器版本,那就不太適用了。

Extjs 提供很完整的日期選擇器,可以設定最早與最晚日期、選擇今天日期、封鎖日期以及日期輸出格式等功能,光這些項目其實就可以符合在大部分情況。

這裡就說明參數選項如何使用

最早日期

minDate: new Date()
minText: '不能選擇比 ' + Ext.Date.format(new Date(), 'Y-m-d') + ' 更早的日期'

最晚日期

maxDate: new Date('01/01/2015')
maxText: '不能選擇比 ' + '2015/01/01 ' + '更晚的日期'

今天顯示字樣

// 今天顯示字樣
todayText: '今天'

封鎖日期

// 封鎖的日期 d/m/Y
disabledDates: ['02/20/2014', '03/21/2014', '04/22/2014', '05/23/2014'], 
// 封鎖的日期的提示
disabledDatesText: '不能選此日期',

按下日期輸出格式

handler: function (picker, date) {
    alert(
        '原本:'+date+
        '\nExt.Date.format(date, Y-m-d)後:' + Ext.Date.format(date, 'Y-m-d')
    )
}

更可以方便選擇年與月

完整程式碼為:
// 載入會用到的程式
Ext.require([
    'Ext.Date.*',
    'Ext.panel.*',
    
]);

Ext.onReady(function () {

    // 建立 form 框架
    Ext.create('Ext.panel.Panel', {
        title: 'Choose a future date:',
        width: 400,
        bodyPadding: 10,
        renderTo: Ext.getBody(),
        items: [{
            // 指定型態為datepicker
            xtype: 'datepicker',
            // 最早的日期 : 今天
            minDate: new Date(),
            minText: '不能選擇比 ' + Ext.Date.format(new Date(), 'Y-m-d') + ' 更早的日期',
            // 最晚的日期 : 01/01/2015
            maxDate: new Date('01/01/2015'),
            maxText: '不能選擇比 ' + '2015/01/01 ' + '更晚的日期',
            // format 日期格式
            // format: 'Y/m/d', 
            // 今天顯示字樣
            todayText: '今天',
            // 封鎖的日期 d/m/Y
            disabledDates: ['02/20/2014', '03/21/2014', '04/22/2014', '05/23/2014'], 
            // 封鎖的日期的提示
            disabledDatesText: '不能選此日期',
            // date 選到的日期
            handler: function (picker, date) {

                alert(
                     '原本:'+date+
                    '\nExt.Date.format(date, Y-m-d)後:' + Ext.Date.format(date, 'Y-m-d')
                    )
            }
        }]
    });


});