2014年12月19日 星期五

升級 Microsoft Azure Storage 4.3.0 以節省輸出流量、加快速度

使用 Microsoft Azure Storage 來擷取 Azure Table 的資料,通常要考慮速度與流量的最佳化,但原本的版本是 Microsoft Azure Storage 2.0.6,是沒有支援 OData 的,且 SDK 又是以 URL 去和 Azure Table 要資料,且一次都會要所有欄位資料,且速度也很慢,更別說還要做一些資料處理了。

我舉我的 Log 為例:

Log 資料欄位約略有 10 欄。
TableQuery query = (new TableQuery());
var data = table.ExecuteQuery(query);

var _query = (
    from entity in data
    // where entity.ErrorMessage != ""
    where entity.Timestamp > DateTime.Now.AddDays(-1)

    // where entity.Location.EndsWith("PUT")
    orderby entity.Timestamp descending
    select entity
);
DateTime std = DateTime.Now;

var r = _query.ToList();

DateTime etd = DateTime.Now;
TimeSpan diffDT = etd.Subtract(std);
Console.WriteLine("DateTime Method : {0} ms", diffDT.TotalMilliseconds);

資料有 370 筆,執行時間 25275.3736 ms,約 25 秒。

使用 Fiddler 來看:


由於 Azure Table 資料輸出有限制筆數,所以才會要那麼多次,每一份資料都 760000 bytes 左右,0.76 MB 左右。

升級 Microsoft Azure Storage 4.3.0 後,將程式碼稍微改一下,抓出需要使用的欄位就好:
TableQuery query = (new TableQuery()).Select(new string[] { "partitionKey", "Timestamp" });

資料有 330 筆,執行時間 9779.4132 ms,約 9.7 秒。

使用 Fiddler 來看:

這樣一來,既省輸出流量,也縮短時間,所以還沒有升級不訪升級試看看。



2014年11月25日 星期二

ASP.NET MVC 解決加入「控制器」( Controller ) 消失的問題

前陣子將專案升級,在 NuGet 安裝 ASP.NET Web API 2.2 for OData v4.0 套件,產生許多 dll 版本錯誤之問題。

修正後,又發現在要加入「控制項」( Controller ) 時,找不到該選項,如下圖:


甚至連加入「區域」( Areas ) 也找不到。

後來找到 visual studio 2013 'add controller' missing 文章解說,照以下方法即可解決:

1.

卸載專案

2.

編輯專案檔

3.

尋找 ProjectTypeGuids 標籤 ( 請先備分 ),將內容改為
<ProjectTypeGuids>{E3E379DF-F4C6-4180-9B81-6769533ABE47};{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>

儲存

4.

重新載入專案,完成。





2014年8月8日 星期五

ASP .NET MVC4 WebApi - 不允許路由(Route)跨區域(Area)找Controller

目前專案設定權限是採用Area進行區分,基本規劃為角色 1..n Area

最近在強化權限控管,發現在Area找不到Controller的時候,竟然會去其他的Area找有沒有符合名稱的Controller(如何切換 Area 的 Web Api 路由)

對規劃的權限區分來說,這一個非常嚴重的問題


舉個例子:
(角色=>Area)
人員管理者=>People
薪資管理者=>Salary

王小明有人員管理者這個角色,所以他可以使用People裡的所有Controller, 碰巧王小明知道了Salary的某隻Api路徑可以取得全公司員工的薪資(api/Salary/AllPeople), 就權限管理機制,王小明沒有Salary的Area權限,所以被判定為無權限(401)。

到這邊都很沒問題,一切都很美好...如果這樣就結束,還要這篇幹嘛

偏偏王小明很有求知的精神,想說只要是People就會被放行,那如果把Salary改成People不知道可不可以?
於是原本的路徑(api/Salary/AllPeople)變成了(api/People/AllPeople)。

這時候權限機制判斷王小明是人員管理者,可以訪問People的Area,所以放行讓他通過, 殊不知在People下沒有AllPeople的Controller,但對權限機制來說並不重要,有沒有是ControllerSelector的問題。

這時候AreaHttpControllerSelector出馬了,經過一連串的檢查,發現People下沒有AllPeople的Controller,沒辦法走下去,那就交給老爸(DefaultHttpControllerSelector)去處理吧。

而老爸是不管這些Area的,只要符合ControllerName就好,所以找到了Salary的AllPeople,然後就跟小弟(ApiControllerActionSelector)說,從Salary的AllPeople找符合的Action來執行吧。

接著就會發現王小明引發美麗錯誤的結果,他拿到不該拿的資料了...其實也還好


這樣是不行的,如果Route有接到area的話,就只能從指定的Area下找Controller,沒找到就丟錯誤出去,所以修改一下AreaHttpControllerSelector
...略...
        private HttpControllerDescriptor GetApiController(HttpRequestMessage request)
        {
            var controllerName = base.GetControllerName(request);

            var areaName = GetAreaName(request);
            if (string.IsNullOrEmpty(areaName))
            {
                return null;
            }

            var type = GetControllerTypeByArea(areaName, controllerName);
            if (type == null)
            {
                //原本回傳null是為了向老爸求救,今天兒子要獨當一面了
                //return null;
                throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.NotFound,
                    string.Format("No route providing a controller name was found to match request URI \"{0}\"", new object[] { request.RequestUri })));
            }

            return new HttpControllerDescriptor(_configuration, controllerName, type);
        }


參考:
如何切換 Area 的 Web Api 路由
DefaultHttpControllerSelector.cs Source Code
ApiControllerActionSelector.cs Source Code

2014年7月11日 星期五

時區到各瀏覽器顯示時間統一處理方式

今天要討論的是,當日期時間資料送到瀏覽器時,顯示出來的結果往往讓人難以捉摸。

DateTimeDateTimeOffset轉成JSON以後的結果:
{
  "DateTimeOffset": "2014-07-11T00:00:00+08:00",
  "DateTime": "2014-07-11T00:00:00"
}
DateTimeOffset後面多了+08:00,表示這個時間是特定時區的時間,而DateTime是沒有的。

這時候丟到JavaScript的Date裡面:
  new Date(DateTimeOffset);
  new Date(DateTime);
呈現在畫面上就會變成:
Browser DateTimeOffset DateTime
Chrome Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間) Fri Jul 11 2014 08:00:00 GMT+0800 (台北標準時間)
IE11 Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間) Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間)
FireFox Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間) Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間)
當沒有指定時區時,顯示出來的結果因各瀏覽器定義不同,而有不同的結果。

當不想要資料庫所有欄位都記時區的時候,就需要變更一下Json的DateTimeConverter

首先宣告一個UTCDateTimeConverter,並且繼承IsoDateTimeConverter ,然後在轉JSON的時候自動轉換成設定的格式

備註1:希望給瀏覽器自動轉換為Local時間,所以是轉換成UTC(國際標準時間)
備註2:避免日後Newtonsoft.Json對IsoDateTimeConverter更新,所以不重寫WriteJson,使用設定轉換格式並由IsoDateTimeConverter的WriteJson進行轉換。
/// <summary>
/// DateTime/DateTimeOffset 傳換為UTC日期格式 (Ex: 2014-07-11T12:53:00+00:00).
/// </summary>
public class UTCDateTimeConverter : IsoDateTimeConverter
{
  /// <summary>
  /// 日期轉成Json格式
  /// </summary>
  /// <param name="writer" >The JsonWriter to write to.</param>
  /// <param name="value" >The value.</param>
  /// <param name="serializer" >The calling serializer.</param>
  public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
  {
    //DateTimeOffset轉換時會自動加上時區,所以要先把他轉成UTC時間的DateTime
    if (value.GetType() == typeof(DateTimeOffset))
    {
      DateTimeOffset tmpDateTimeOffset = (DateTimeOffset)value;
      value = tmpDateTimeOffset.DateTime.Add(-tmpDateTimeOffset.Offset);
    }
    //設定想要轉換的格式
    base.DateTimeFormat = "yyyy-MM-ddTHH:mm:ss.FFFFFFFK+00:00";
    //使用預設的WriteJson進行轉換
    base.WriteJson(writer: writer, value: value, serializer: serializer);
  }
}
因為希望傳出去時間格式是統一的,而使用的也是WebApi,所以直接在App_Start/WebApiConfig.cs進行設定:
config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new UTCDateTimeConverter());

轉出來的Json為:
{
  "DateTimeOffset": "2014-07-10T16:00:00+00:00",
  "DateTime": "2014-07-11T00:00:00+00:00"
}
DateTimeOffset已經是被剪了8小時。

呈現在畫面上就會變成:
Browser DateTimeOffset DateTime
Chrome Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間) Fri Jul 11 2014 08:00:00 GMT+0800 (台北標準時間)
IE11 Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間) Fri Jul 11 2014 08:00:00 GMT+0800 (台北標準時間)
FireFox Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間) Fri Jul 11 2014 08:00:00 GMT+0800 (台北標準時間)


瀏覽器都統一了^^

參考:
IsoDateTimeConverter Source
Add different Json.NET DateTimeConverters through the JsonFormatter's SerializerSettings
Create a JSON.NET Date to String custom Converter

2014年7月8日 星期二

ASP .NET MVC4 WebApi-CultureInfo-文化特性排序

相信很多人都發生過,本機排序正常,一上到正式主機以後,排序出來的結果卻不一樣。

其實.NET的執行緒就跟SQL Server的資料表一樣有定序問題,所以排序結果會因為設定而有所不同,基本上執行緒的定序是跟著Server預設語系的。

在變更執行緒的語系之前,先看一下CultureInfo類別

MSDN的說明是:提供有關特定文化特性 (Culture) 的資訊 (文化特性在 Unmanaged 程式碼開發中稱為「地區設定」(Locale))。 提供的資訊包括文化特性的名稱、書寫系統、使用的曆法,以及日期和排序字串的格式。

最後面那一句話,排序字串的格式

了解了這個類別的用途,接著來看一下幾個語系排序的規則。

文化特性名稱
文化特性
預設的排序名稱和識別項
替代排序名稱和識別項
zh-TW
中文 (台灣)
筆劃:0x00000404
注音符號:0x00030404
zh-CN
中文 (中華人民共和國)
發音:0x00000804
筆劃:0x00020804
ja-JP
日文 (日本)
預設:0x00000411
Unicode:0x00010411

知道各語系排序規則以後,接著我們只需要在排序前加上一個指定就搞定了。

System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(language);

這樣是不是很簡單呢。(明明為了排序搞了很久)

參考:
CultureInfo與中文字串排序
CultureInfo 類別


2014年5月19日 星期一

執行 API Controller 遇到 「Not running in a hosted service or the Development Fabric.」 問題之解決方案

建置 API Controller 時,執行後遇到以下錯誤訊息:

Not running in a hosted service or the Development Fabric.

描述: 在執行目前 Web 要求的過程中發生未處理的例外狀況。請檢閱堆疊追蹤以取得錯誤的詳細資訊,以及在程式碼中產生的位置。

例外狀況詳細資訊: System.InvalidOperationException: Not running in a hosted service or the Development Fabric.

原始程式錯誤:

在執行目前 Web 要求期間,產生未處理的例外狀況。如需有關例外狀況來源與位置的資訊,可以使用下列的例外狀況堆疊追蹤取得。

堆疊追蹤:

[InvalidOperationException: Not running in a hosted service or the Development Fabric.]
   Microsoft.WindowsAzure.Diagnostics.DiagnosticMonitor.GetDefaultStartupInfoForCurrentRoleInstance() +173
   Microsoft.WindowsAzure.Diagnostics.DiagnosticMonitorTraceListener..ctor() +59

[ConfigurationErrorsException: 無法建立 Microsoft.WindowsAzure.Diagnostics.DiagnosticMonitorTraceListener, Microsoft.WindowsAzure.Diagnostics, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35。]
   System.Diagnostics.TraceUtils.GetRuntimeObject(String className, Type baseType, String initializeData) +6707013
   System.Diagnostics.TypedElement.BaseGetRuntimeObject() +45
   System.Diagnostics.ListenerElement.GetRuntimeObject() +83
   System.Diagnostics.ListenerElementsCollection.GetRuntimeObject() +142
   System.Diagnostics.TraceInternal.get_Listeners() +181
   System.Diagnostics.TraceInternal.TraceEvent(TraceEventType eventType, Int32 id, String format, Object[] args) +155
   System.Diagnostics.Trace.TraceInformation(String message) +14
   System.Web.Http.Tracing.SystemDiagnosticsTraceWriter.TraceMessage(TraceLevel level, String message) +157
   System.Web.Http.Tracing.SystemDiagnosticsTraceWriter.Trace(HttpRequestMessage request, String category, TraceLevel level, Action`1 traceAction) +438
   System.Web.Http.Tracing.ITraceWriterExtensions.TraceBeginEndAsync(ITraceWriter traceWriter, HttpRequestMessage request, String category, TraceLevel level, String operatorName, String operationName, Action`1 beginTrace, Func`1 execute, Action`2 endTrace, Action`1 errorTrace) +464
   System.Web.Http.Tracing.Tracers.RequestMessageHandlerTracer.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) +367
   System.Net.Http.DelegatingHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) +41
   System.Web.Http.HttpServer.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) +390
   System.Net.Http.HttpMessageInvoker.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) +55
   System.Web.Http.WebHost.HttpControllerHandler.BeginProcessRequest(HttpContextBase httpContextBase, AsyncCallback callback, Object state) +316
   System.Web.Http.WebHost.HttpControllerHandler.System.Web.IHttpAsyncHandler.BeginProcessRequest(HttpContext httpContext, AsyncCallback callback, Object state) +77
   System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +301
   System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +155

這因為是 Windows Azure 雲端服務專案發生的情況,所以解決方法有二:

1.

建置 Windows Azure 雲端服務會有兩個專案,一為 ASP.NET MVC 4 Web 應用程式,另一個是 Windows Azure Web Role 專案。必須在方案內設定 Windows Azure Web Role 專案為「啟用」,像我就是設定 ASP.NET MVC 4 Web 應用程式 為「啟用」。

2.

web.config 中移除追蹤接聽項 ( trace listener ):
<trace>
  <listeners>
    <add type="Microsoft.WindowsAzure.Diagnostics.DiagnosticMonitorTraceListener, Microsoft.WindowsAzure.Diagnostics, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
      name="AzureDiagnostics">
      <filter type="" />
    </add>
  </listeners>
</trace>  

兩種方法都能解決此種問題,我是用第二種方法解決。


2014年5月15日 星期四

Entity Framework 與 LINQ -- 篩選(Where)使用時機

最近發現,使用Entity Framework的話,要注意下Where的時機,因為有可能是會在SQL端篩選,也有可能會在C#端篩選,可能會導致不同的結果,甚至發生不支援的情況。

拿字串A是否包含了字串B作為範例,這時候在SQL端(還沒產生成實體物件)時執行結果 跟 C#端(已從DB撈出資料)執行結果 就會發生不同的狀況。
先來看一Where下在資料還沒產生成實體物件的時候
C#:
var query = db.Table.Where(x => x.Name.Contains("abc")).ToList();
SQL執行語法會是
SELECT[EmployeeId],[Name]
FROM [dbo].[Employee]
WHERE [Name] LIKE '%abc%'
由此可見,資料判斷是在SQL端處理,然後把結果丟回到C#端。

再來是Where下在ToList()後面
C#:
var query = db.Table.ToList().Where(x => x.Name.Contains("abc"));
SQL執行語法會是
SELECT[EmployeeId],[Name]
FROM [dbo].[Employee]
這時候變成了把所有Employee資料撈出來以後,在篩選資料。

一個是在SQL端下LIKE,一個是在C#端執行Contains,都是包含"abc",但是結果就會不一樣囉。

SQL的LIKE是不分辨大小寫的,所以"ABC"、"AbC"、"abc"、"abC"...都是會被認為是OK的資料。
但是在C#的Contains是有區分大小寫的,所以只有"abc"才會被篩選出來。

如果使用IndexOf去指定不區分大小寫,但是這個只能在C#端使用
錯誤的C#語法:
var query = db.Table.Where(x => x.Name.IndexOf("abc", StringComparison.CurrentCultureIgnoreCase) >= 0).ToList();
這樣會報錯,因為LINQ在轉SQL語法的時候會發生不支援的情況,就回歸到一開始提到的Where時機。
所以我們要在取出資料後,由C#去執行篩選
C#語法:
var query = db.Table.ToList().Where(x => x.Name.IndexOf("abc", StringComparison.CurrentCultureIgnoreCase) >= 0);

PS:Contains是使用IndexOf去實做出來的,所以基本上IndexOf效能會略好於Contains



2014年5月9日 星期五

ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount(續2) -- 中繼傳遞OData參數

延續前二篇
(ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount)
(ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount(續) -- C# Model 接取$inlinecount 資料)

範例為建立一個API,把OData參數傳給資料來源Api,然後做簡單處理或直接回傳(資料來源Api開放使用OData)。

建立一個Api
//如果有$inlinecount就會是不同的格式,所以這邊回傳Object
public Object Get()
{
    return "test";
}
取得所有OData參數
List<string> arrParams = Request.GetQueryNameValuePairs() //所有QueryString的Name、Value集合
    .Where(x => x.Key.StartsWith("$")) //只取得開頭為$
    .Select(x => string.Format("&{0}={1}", x.Key, Uri.EscapeDataString(x.Value))) //組回 &Name=Value
    .ToList();
因為範例的資料來源API本身有指定參數,所以這邊會先把 & 加上,方便後面整理所有參數,請視個人情況調整加上的時機。

判斷是否有設定$inlinecount=allpages
有的話就要用C# Model 接取$inlinecount 資料的共用Model去接資料,沒有就用一般方式即可。
if(arrParams.Select(x => x.ToLower()).Contains("&$inlinecount=allpages") == true)
Select出來轉小寫以後判斷是否包含"&$inlinecount=allpages",因為沒實際測試$filter大小寫是否有區別,所以沒有在取得QueryString的時候就轉。
由組回 Name=Value 時前面有沒有加 & 來決定這邊要不要加。

組合資料來源Api的URL
string.Format("DefauleUrl?name1=val1&name2=val2{0}", string.Join("", arrParams));
string.Join要不要加 & 一樣由前面就決定了,串出來的Url請仔細確認參數部分的格式是不是正確的。

下面是實際範例
public Object Get()
{
    List<string> arrParams = Request.GetQueryNameValuePairs().Where(x => x.Key.StartsWith("$")).Select(x => string.Format("&{0}={1}", x.Key, Uri.EscapeDataString(x.Value))).ToList();
    bool IsAllpages = false;
    string strParams = string.Join("", arrParams);
 string strUrl = string.Format("DefauleUrl?name1=val1&name2=val2{0}", string.Join("", arrParams));
    List<ModelName> Models = new List<ModelName>();
    ODataByApi<ModelName> odateModel = new ODataByApi<ModelName>();
    if (arrParams.Select(x => x.ToLower()).Contains("&$inlinecount=allpages"))
    {
        odateModel = GetData<ODataByApi<ModelName>>(strParams);
        Models = odateModel.Items.ToList();
        IsAllpages = true;
    }
    else
    {
        Models = GetData<List<ModelName>>(strParams);
    }
 //資料處理
    if (Models != null)
    {
        //To do....
    }
 //有$inlinecount=allpages,丟回ODataByAp後return
    if (IsAllpages == true)
    {
        odateModel.Items = Models;
        return odateModel;
    }
 //否則return List
    return Models;
}

參考:
(ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount)
(ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount(續) -- C# Model 接取$inlinecount 資料)


2014年5月7日 星期三

ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount(續) -- C# Model 接取$inlinecount 資料

延續前一篇(ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount)

當C#去接API來的資料,且有設定$inlinecount,資料格式會變更成:
{
  "Items": [
    {
      //資料1
    },
    {
      //資料2
    },
    {
      //資料3
    }
  ],
  "NextPageLink": null,
  "Count": 3
}
Items存放資料,NextPageLink存放下一頁網址,Count存放數量

無法使用一般Model去接資料,因此需要準備一個共用Model
public class ODataByApi<T>
{
    public ICollection<T> Items { get; set; }
    public string NextPageLink { get; set; }
    public int Count { get; set; }
}

用共用Model,並指定資料Model去接資料即可
JsonConvert.DeserializeObject<ODataByApi<ModelName>>(strJson);


參考:
(ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount)

2014年5月5日 星期一

ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount

WebApi提供各種平台取得相關資料,為了滿足各種平台不同的需求(排序、分頁、查詢等等),最直覺的方式就是指定參數給他們使用,再處理各個參數,但這樣就變成不夠彈性,不需要查詢也要傳參數的值,同時加重了前、後端開發人員的麻煩。

使用OData讓前端傳入參數,不需要寫死程式,隨心所欲的完成排序、分頁、查詢等功能。
先來看看OData常用參數:
$top傳回前幾筆資料
$skip跳過幾筆資料
$filter
  • 查詢(where)
    eq-等於、gt-大於、lt-小於、ne-不等於
  • 串連
    and、or
  • $orderby排序
    $inlinecount傳回資料、總筆數、下一頁Url
  • allpages
  • none(預設)
  • 使用方式跟一般傳參數一樣,例如:http://{domain}/api/{controller}/{action}?$top=5&$skip=10
    (更多參數說明請參考官方網站)

    在API的部分,需要設定屬性[Queryable],就可以使用OData。
    備註:
    1.許多文章都說需要使用[Queryable]搭配回傳IQuerable、AsQueryable(),經過測試後,回傳IEnumerable也是可以正常使用的。
    2.如果不想開放所有參數、或有一些限制條件,後端人員可以在[Queryable]設定相關參數以達到目的

    這樣就可以依照各種情況由前端開發人員自行決定需要傳遞那些參數,而後端開發人員只需要專心的處理資料以確保資料正確性即可。


    在測試的時候發現$inlinecount一直沒辦法使用(微軟好像不支援一些參數),這樣前端在做分頁時就不知道資料總筆數,所以我們動手實作一個屬性吧

    public class InlineCountQueryableAttribute : QueryableAttribute
        {
            private static MethodInfo _createPageResult =
                typeof(InlineCountQueryableAttribute)
                .GetMethods(BindingFlags.Static | BindingFlags.NonPublic)
                .Single(m => m.Name == "CreatePageResult");
    
            public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
            {
                base.OnActionExecuted(actionExecutedContext);
    
                HttpRequestMessage request = actionExecutedContext.Request;
                HttpResponseMessage response = actionExecutedContext.Response;
    
                IQueryable result;
                if (response.IsSuccessStatusCode
                    && response.TryGetContentValue<IQueryable>(out result))
                {
                    long? inlineCount = request.GetInlineCount();
                    if (inlineCount != null)
                    {
                        actionExecutedContext.Response = _createPageResult.MakeGenericMethod(result.ElementType).Invoke(
                            null, new object[] { request, request.GetInlineCount(), request.GetNextPageLink(), result }) as HttpResponseMessage;
                    }
                }
            }
    
            internal static HttpResponseMessage CreatePageResult<T>(HttpRequestMessage request, long? count, Uri nextpageLink, IEnumerable<T> results)
            {
                return request.CreateResponse(HttpStatusCode.OK, new PageResult<T>(results, nextpageLink, count));
            }
        }
    
    設定屬性由[Queryable]改為[InlineCountQueryable]即可正常使用$inlinecount參數

    參考:
    System.Web.Http.OData 命名空間
    OData官網
    關於IQueryable特性的小實驗
    Web API, OData, $inlinecount and testing


    2014/07/22 KaiYai補充:
    最近無意間發現了Bug,當程式碼出現錯誤產生Exception,Action回前端時依然會進到Attribute內執行OnActionExecuted事件,因為是Exception,所以傳進來的HttpActionExecutedContext.Response會是null,而HttpActionExecutedContext.Exception會是錯誤資訊,如果利用原本程式碼收到的錯誤訊息不會是原本實際發生錯誤的部分,而是會出現OnActionExecuted內的錯誤:
    <Error>
        <Message>發生錯誤。</Message>
        <ExceptionMessage>並未將物件參考設定為物件的執行個體。</ExceptionMessage>
        <ExceptionType>System.NullReferenceException</ExceptionType>
        <StackTrace>
            ...略...
        </StackTrace>
    </Error>
    

    但實際上的錯誤應該是
    <Error>
        <Message>發生錯誤。</Message>
        <ExceptionMessage>輸入字串格式不正確。</ExceptionMessage>
        <ExceptionType>System.FormatException</ExceptionType>
        <StackTrace>
            ...略...
        </StackTrace>
    </Error>
    

    這是因為response已經收到null值,指令沒有判斷到是否為null,就會發生會設定物件的錯誤。
    所以修正條件式加上判斷response != null
    if (response != null && response.IsSuccessStatusCode && response.TryGetContentValue<IQueryable>(out result))

    原本想要直接判斷HttpActionExecutedContext.Exception != null,但想了想,為什麼執行base.OnActionExecuted(actionExecutedContext);沒有發生錯誤呢?所以決定看一下QueryableAttribute的Code,看到下的條件以後,毅然決然的直接照辦,有錯微軟會先被罵
    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
    {
        ...略...
        HttpResponseMessage response = actionExecutedContext.Response;
    
        if (response != null && response.IsSuccessStatusCode) 
        ...略...
    }
    


    參考:
    QueryableAttribute Source(aspnetwebstack /src/System.Web.Http.OData/QueryableAttribute.cs)

    2014年4月30日 星期三

    .Net WebSite/WebApi 限制連線IP

    開發人員最怕的就是未經授權或非正常管道使用網站/網路服務

    小弟目前專案都是透過WebApi傳遞資料,都是一關卡一關

    避免有人不小心知道使用後端服務的方法,所以後端的服務都必須限制連接IP


    首先先在Web.config(會跟IIS限制IP and DomainName同步)設定禁止所有IP連接
    <configuration>
      <system.webserver>
        <security>
          <ipsecurity allowunlisted="flase">
        </ipsecurity>
        </security>
      </system>
    </configuration>
    

    這樣就禁止了所有IP連線,接下來為了方便本機測試功能是否正常,以及允許連線的IP,所以我們要在ipSecurity裡面去設定開放條件
    <configuration>
      <system.webserver>
        <security>
          <ipsecurity allowunlisted="flase">
            <add allowed="true" ipaddress="127.0.0.1" />
            <add allowed="true" ipaddress="192.168.1.1" />
          </ipsecurity>
        </security>
      </system>
    </configuration>
    


    這時候基本上已經完成了9成

    剩下的一成就是要看IIS有沒有開啟IP 位址及網域限制(IP Address and Domain Restrictions)功能

    前幾天因為Server沒有安裝所以卡在這裡(Windows Azure 遠端桌面設定)

    沒有開啟的話,請到開啟或關閉Windows功能去設定

    以上2個動作就完成限制IP連線了


    參考:
    Web.config ipSecurity
    IP Security <ipsecurity>

    ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- 換頁重新載入資料

    在 Web Form 做 Grid 換頁時,通常會先將資料把所有資料先建入 DataTable,再使用控制項接收這些資料,換頁時,就將這些資料經過處理再顯示。然後再做資料搜尋,會再去要一次資料,如此一來,在每次產生 GridView 時,都會使用同一份資料篩選,等於是網頁上有一份資料在做處理,如果資料量一大,吃的資源也大,處理與顯示時相對的就慢。

    使用 Extjs 做 Grid.Panel 分頁,一般來說會先將所有資料載到頁面,再做分頁,只是 Extjs 接收資料格式必須要在最根層加上總數量,這樣才能知道該頁顯示幾頁,總共有幾頁,所以格式必須符合如下:
    {
        "totalCount": "6679",
        "topics": [
        {
            // ....
        },
        {
            // ....
        },
        {
            // ....
        },
        {
            // ....
        },
        {
            // ....
        }
        // ...
        ]
    }
    

    而符合這種格式必須在 Web Api 段做些許調整,要多寫一個屬性讓 API 能使用,可以參考:Web API, OData, $inlinecount and testing,程式碼如下

    public class InlineCountQueryableAttribute : QueryableAttribute
    {
        private static MethodInfo _createPageResult =
            typeof(InlineCountQueryableAttribute)
            .GetMethods(BindingFlags.Static | BindingFlags.NonPublic)
            .Single(m => m.Name == "CreatePageResult");
    
        public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
        {
            base.OnActionExecuted(actionExecutedContext);
    
            HttpRequestMessage request = actionExecutedContext.Request;
            HttpResponseMessage response = actionExecutedContext.Response;
    
            IQueryable result;
            if (response.IsSuccessStatusCode
                && response.TryGetContentValue<IQueryable>(out result))
            {
                long? inlineCount = request.GetInlineCount();
                if (inlineCount != null)
                {
                    actionExecutedContext.Response = _createPageResult.MakeGenericMethod(result.ElementType).Invoke(
                        null, new object[] { request, request.GetInlineCount(), request.GetNextPageLink(), result }) as HttpResponseMessage;
                }
            }
        }
    
        internal static HttpResponseMessage CreatePageResult<T>(HttpRequestMessage request, long? count, Uri nextpageLink, IEnumerable<T> results)
        {
            return request.CreateResponse(HttpStatusCode.OK, new PageResult<T>(results, nextpageLink, count));
        }
    }
    

    在 API 加上此屬性:
    [InlineCountQueryable]
    public IEnumerable GetEmployees()
    {
        IEnumerable employees = db.Employees.ToList();
    
        foreach (Employees employee in employees)
        {
            employee.Employees1 = null;
            employee.Employees2 = null;
        }
    
        return employees;
    }
    

    執行結果如下:
    { 
        Count: 15,
        Items: [
            { ... }, 
            { ... }, 
            ...
        ],
        NextPageLink: XXX,
    }
    

    這樣輸出結果與 Extjs Grid.Panel 做分頁要接收的格式已經 95% 相似了,而換頁時通常自動會帶 page 、 start 和 limit,page 感覺只是顯示用,而 start 和 limit 的部分可以使用 API 的 Queryable 來解決,它等同於 Queryable 的 $skip 和 $top:

    首先我們先看如何在 grid.Panel 加上分頁,加上一個屬性即可,其中 store 為資料來源:
    bbar: Ext.create('Ext.PagingToolbar', {
        store: store,
        displayInfo: true,
        displayMsg: 'Displaying topics {0} - {1} of {2}',
        emptyMsg: "No topics to display",
    }),
    

    因為每一次換頁就會與 store 要一次資料,所以必須針對來源做修改:
    var store = Ext.create('Ext.data.JsonStore', {
        storeId: 'store',
        model: 'Model',
        pageSize: 3,
        
        proxy: {
            type: 'ajax',
            url: 'http://localhost:8090/api/Control?$inlinecount=allpages',
            reader: {
                type: 'json',
                root: 'Items',
                totalProperty: 'Count',
    
            },
            startParam: '$skip',
            limitParam: '$top'
        },
        autoLoad: true,
        listeners: {
            load: function (store, records, options) {
            }
        },
    
    });
    

    其中 PageSize 為每頁顯示數量,而 startParam、limitParam 則是改變它換頁帶的參數,最後執行就可以看到每換一次頁就會重新與 API 要一次資料,這樣一來頁面負擔不會太重,同時又做到速度與流量兼顧的結果。


    2014年4月29日 星期二

    Windows Azure 遠端桌面設定+遠端掛載本機磁碟

    相信大家都遇過本機(或測試Server)Run的程式一切正常(完美),但是一上到正式(或其他)Server就一堆問題,絕大多數是因為有些東西沒有設定好(這邊指的是Server上的一些設定)

    最近使用Windows Azure建立雲端服務(Cloud Service),為了限制IP使用,在Web.config設定允許IP列表,本機、同事的電腦...等等測試一切正常,但是一上到Azure就完全無效

    因為也是最近才接觸Azure,只用過入口網站跟VS進行設定跟上傳,一直有個疑惑,微軟不可能鎖死Server設定(一定被罵翻),因為太多狀況了,也不可能全部開放,但是遠端桌面及一些Server設定應該是可以才對,花了一些時間終於找到了方法

    首先要先設定連線的帳號密碼


    然後會開出設定遠端桌面,這時候敲入使用者名稱、密碼、到期日

    接著將Azure設定好的遠端連線(rdp)檔下載下來

    帳號已經幫你預設好了,敲入密碼就可以連線進去了


    參考:
    微軟Azure測試心得分享(四) :啟用Virtual Machine (中)
    遠端連線至 Windows Azure 雲端服務



    補充:
    透過VS去上傳專案,真的是有夠慢的,只更新DLL也要傳個老半天,雲端服務又不支援FTP,也不想安裝(或自己開發)檔案傳輸工具,透過David Kuo的協助,原來可以透過遠端桌面掛載本機磁碟的方式達到網路硬碟的功效

    利用Azure下載的遠端桌面檔,設定要分享的本機磁碟

    進入後就可以在檔案總管看到我們分享進來的本機磁碟

    這樣傳檔案就真的方便多了

    參考:
    遠端桌面本機與遠端檔案互傳 (MS AZURE 適用)

    2014年4月21日 星期一

    GData APIs ( Google Data APIs ) - Calendar

    個人通常記錄未來工作事項都是習慣使用紙本記錄,因為好處是隨身、任意塗改、做不止文字的記錄,但我相信還是有更多的人較喜歡使用 Google 日曆,除了可以記錄工作事項外,且可將事項分類、重要性、附檔、結合 Gmail....等好處多多 ( 請參閱: Google日曆 最佳化:在忙碌瑣事中有效率掌握最重要行程)。

    此篇文章只提供重要程式碼,去使用 C# Windows Form 來建置一個視窗程式與 Google 日曆互動。

    未下載 GData APIs 且未安裝請參考:GData APIs ( Google Data APIs ) 介紹與安裝

    引用參考

    using Google.GData.Calendar;
    using Google.GData.Client;
    using Google.GData.Extensions;
    

    建立 CalendarService

    service = new CalendarService(projectid);
    service.setUserCredentials(username, password);
    

    若無 projectid,請到 Google Developers Console 申請。

    取得 使用者 所有日曆

    public void GetUserCalendars()
    {
        FeedQuery query = new FeedQuery();
        query.Uri = new Uri("http://www.google.com/calendar/feeds/default");
    
        AtomFeed calFeed = service.Query(query);
    
        for (int i = 0; i < calFeed.Entries.Count; i++)
        {
            // Console.WriteLine(calFeed.Entries[i].Title.Text);
            // do something ...
        }
    }
    

    取得所有日曆事件

    public AtomEntryCollection GetAllEvents()
    {
        EventQuery myQuery = new EventQuery(calendarURI);
        EventFeed myResultsFeed = service.Query(myQuery) as EventFeed;
    
        entries = myResultsFeed.Entries;
    
        return entries;
    }
    
    calendarURI 為 https://www.google.com/calendar/feeds/default/private/full。

    取得日曆 日期區間 事件

    public AtomEntryCollection GetDateRangeEvents(DateTime startTime, DateTime endTime)
    {
        EventQuery myQuery = new EventQuery(calendarURI);
        myQuery.StartTime = startTime;
        myQuery.EndTime = endTime;
    
        EventFeed myResultsFeed = service.Query(myQuery) as EventFeed;
    
        entries = myResultsFeed.Entries;
    
        return entries;
    }
    

    建立事件

    public void CreateEvent(string title, string content, DateTime dtStart, DateTime dtEnd)
    {
        EventEntry entry = new EventEntry();
    
        entry.Title.Text = title;
        entry.Content.Content = content;
    
        When eventTime = new When(dtStart, dtEnd);
        entry.Times.Add(eventTime);
    
        Uri postUri = new Uri(calendarURI);
    
        AtomEntry insertedEntry = service.Insert(postUri, entry);
    
    }
    

    更新事件

    public void UpdateEvent(EventEntry entry)
    {
        Uri postUri = new Uri(calendarURI);
    
        AtomEntry insertedEntry = service.Update(entry);
    
    }

    實作

    我用以上幾個函式寫了一個視窗程式,下圖為成品




    2014年4月13日 星期日

    GData APIs ( Google Data APIs ) - Blogger

    現在大部分的部落格都已經開放 API 讓開發人員能使用 API 對部落格做 CRUD 的動作,痞克邦、隨意窩、Blogger,甚至包括之前關站的無名小站皆有提供,接下來就開始使用 C# 來撰寫使用 Blogger 的程式。

    有許多人都使用 webBrowser 來模擬瀏覽器的動作發布文章,但是 blogger 無法讓你這樣做,你可以試試看,保證你抓不到 tag。

    未下載 GData APIs 且未安裝請參考:GData APIs ( Google Data APIs ) 介紹與安裝

    引用參考

    使用 Google.GData.Client.dll 。
    using Google.GData.Client;
    

    建立 Service

    username 為帳號 ( Email ),password 為密碼,若帳號有設置兩步驗證的話,請到 應用程式專用密碼 去取得一組密碼,要不然原本帳號密碼是無法使用。
    service = new Service("blogger", "blogger-example");
    service.Credentials = new GDataCredentials(username, password);
    

    取得文章列表

    max-results 為參數,讓回傳的文章數最大為 500 篇。
    /* 列出使用者的部落格 */
    public Dictionary<string, Uri> GetListUserBlogs()
    {
        Dictionary<string, Uri> dicBlogs = new Dictionary<string,Uri>();
    
        FeedQuery query = new FeedQuery();
        query.Uri = new Uri("http://www.blogger.com/feeds/default/blogs");
        AtomFeed feed = service.Query(query);
    
        // 發佈文章
        Uri blogPostUri = null;
    
    
        if (feed != null)
        {
            foreach (AtomEntry entry in feed.Entries)
            {
                
                for (int i = 0; i < entry.Links.Count; i++)
                {
                    if (entry.Links[i].Rel.Equals("http://schemas.google.com/g/2005#post"))
                    {
                        blogPostUri = new Uri(entry.Links[i].HRef.ToString() + "?max-results=500");
                        dicBlogs.Add(entry.Title.Text, blogPostUri);
                        
                    }
                }
                // return blogPostUri;
                
            }
        }
        return dicBlogs;
    }
    

    新增文章

    其中 tags 變數為逗點分隔的字串,isDraft 為是否為草稿。
    /* 建立新文章且將它送到 blogPostUri */
    public AtomEntry CreatePost(Uri blogPostUri, string title, string content, string tags, DateTime dt, bool isDraft)
    {
        AtomEntry createdEntry = null;
        if (blogPostUri != null)
        {
            // construct the new entry
            AtomEntry newPost = new AtomEntry();
            newPost.Title.Text = title;
            newPost.Content = new AtomContent();
            newPost.Content.Content = content;
    
            foreach (string term in tags.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries))
                newPost.Categories.Add(new AtomCategory(term, new AtomUri("http://www.blogger.com/atom/ns#")));
    
            newPost.Published = dt;
            newPost.IsDraft = isDraft;
    
            createdEntry = service.Insert(blogPostUri, newPost);
            if (createdEntry != null)
            {
                Console.WriteLine("  New blog post created with title: " + createdEntry.Title.Text);
            }
        }
        return createdEntry;
    }
    

    更新文章


    /* 編輯文章 */
    public void EditEntry(AtomEntry toEdit)
    {
        if (toEdit != null)
        {
            // toEdit.Title.Text = "Marriage Woes!";
            toEdit = toEdit.Update();
        }
    }
    

    刪除文章


    /* 刪除文章 */
    public void DeleteEntry(AtomEntry toDelete)
    {
        if (toDelete != null)
        {
            toDelete.Delete();
        }
    }
    

    實作

    我用以上幾個函式寫了一個視窗程式,下圖為成品




    2014年4月10日 星期四

    GData APIs ( Google Data APIs ) 介紹與安裝

    GData 是一種簡單的標準協議,用於網路資料的讀寫。它結合了常見的基於 xml 的數據聚合格式( Atom 與 RSS)以及基於 Atom 發布協議的 Feed 發布體系,並擴展了部分功能用於處理查詢功能。有時,我們需要發送一個查詢請求給服務器,並得到服務器返回的相符的查詢結果,而目前的 Atom 和 RSS 標準都不具備這一功能。GData 讓使用者可以使用聚合 ( syndication ) 的機制來發送請求並接收結果,它使你可以發送數據給 Google,更新那些 Google 已經擁有的資料。

    GData 擴展了原有的 RSS 和 Atom 協議,使其從一種單向的聚合變成了雙向的互動,這似乎是大家都在探索的 feed 的未來發展方向,比如微軟的 SSE。

    Joe Gregorio 認為,GData 是將RSS、Atom,尤其是 Atom 發布協議 ( Atom Publishing Protocol )與 Amazon 的 Openserch 標準相結合;Maurice Codik 認為 GData 標準使 Google 的數據更加開放,各種應用之間可以更方便地利用這一標準來使用數據;甚至有人認為這使得基於 Google 各種應用的企業門戶雛形開始顯現。

    引用:什么是GData(Google Data API)

    安裝

    首先到 google-gdata 下載並安裝,安裝後路徑在 「 C:\Program Files (x86)\Google\Google Data API SDK\Redist 」,可以找到所有的 dll,往後可以找此路徑下的 dll 引用,而 「 C:\Program Files (x86)\Google\Google Data API SDK\Documentation\Google.GData.Documentation.chm 」 就是使用文件檔。


    此篇文章只是稍微介紹與安裝 GData APIs,往後會有系列文章介紹如何使用 dll。

    2014年4月7日 星期一

    ASP.NET MVC 4 Controller 使用委派 ( delegate ) 做更進階的篩選

    委派是事件的基礎。

    將委派與具名方法或匿名方法建立關聯,即可具現化 (Instantiated) 委派。 如需詳細資訊,請參閱具名方法匿名方法
    委派必須以具有相容傳回型別和輸入參數的方法或 Lambda 運算式具現化。 如需方法簽章中可允許之變異數等級的詳細資訊,請參閱委派中的變異數 (C# 和 Visual Basic)。 若要搭配匿名方法使用,就要同時宣告委派以及其相關聯的程式碼。

    以上引用:delegate (C# 參考)

    方便處在於可以在類別上宣告後,即可在各個方法函數內使用,可以依照方法不同,而產生結果也不同,但是只用同一委派 ( delegate ) 實現。

    這裡使用北風資料庫來實作委派,沒有資料庫的話可以參考:Visual Studio 2012 安裝 Northwind 資料庫並建立 Entity Framework Database First ( .edmx )

    在 Controller 中撰寫,傳入訂單編號來搜尋訂單下的訂單細項之產品的名稱,單位價格區間。

    先看看資料庫關聯表:

    先宣告委派,並且決定傳入傳出參數,關鍵字、最小價格、最大價格:
    delegate bool SearchProduct(Order_Details order_detail, string keyword, decimal mixprice, decimal minprice);
    

    類別內宣告委派:
    SearchProduct sp;
    

    最後在 Controller 實作並且使用委派:
    [HttpGet]
    public IEnumerable<Order_Details> SearchProduct(int orderid, string keyword, decimal minprice, decimal maxprice)
    {
        sp = (o, k, min, max) =>
        {
    
            return o.Products.ProductName.Contains(k) ||
                   o.Products.UnitPrice >= min &&
                   o.Products.UnitPrice <= max;
        };
    
        var Order_Details = db.Order_Details
            .Include(x => x.Products)
            .Where(x => x.Orders.OrderID == orderid)
            .ToList()
            .Where(x => sp(x, keyword, minprice, maxprice));
    
        foreach (Order_Details _order_detail in Order_Details)
        {
            _order_detail.Products.Order_Details = null;
        }
    
        return Order_Details; 
    }
    

    執行後呼叫 API - http://localhost:8090/api/Product/SearchProduct?orderid=10285&keyword=Ch&maxprice=18&minprice=17,得到結果:
    [
        {
            "OrderID": 10285,
            "ProductID": 1,
            "UnitPrice": 14.4,
            "Quantity": 45,
            "Discount": 0.2,
            "Orders": null,
            "Products": {
                "ProductID": 1,
                "ProductName": "Chai",
                "SupplierID": 1,
                "CategoryID": 1,
                "QuantityPerUnit": "10 boxes x 20 bags",
                "UnitPrice": 18,
                "UnitsInStock": 39,
                "UnitsOnOrder": 0,
                "ReorderLevel": 10,
                "Discontinued": false,
                "Categories": null,
                "Order_Details": null,
                "Suppliers": null
            }
        }
    ]
    

    為何不在 LINQ 內使用條件判斷? 其實我已經實作過了,會出現「運算式樹狀架構可能不含指派運算子」錯誤,且必須要實例化出來才可以做,以下錯誤程式碼:
    [HttpGet]
    public IEnumerable<Order_Details> SearchProduct(int orderid, string keyword, decimal minprice, decimal maxprice)
    {
    
        Products _product;
    
        var Order_Details = db.Order_Details
            .Include(x => x.Products)
            .Where(x => x.Orders.OrderID == orderid &&
                (
                    (_product = x.Products) != null &&
                    _product.ProductName.Contains(keyword) ||
                    _product.UnitPrice >= minprice ||
                    _product.UnitPrice <= maxprice
                )
    
            ).ToList();
    
        return Order_Details; 
    }
    


    2014年4月4日 星期五

    ASP.NET MVC 4 Model 中 Virtual 的作用

    在 View 端專寫時,顯示某一張表的資料,若未使用到相對應的關聯表,則不載入此表資料;若使用,則載入。這個作用就是延遲載入 ( Lazy Load ),使用再載入,很類似非同步的概念,而在 Model 關聯成員加上 Virtual 關鍵字,就可以做到延遲載入的作用。

    寫個例子來表示一下差別,資料庫使用北風資料庫利用 Database first 建置,可以參考 Visual Studio 2012 安裝 Northwind 資料庫並建立 Entity Framework Database First ( .edmx )

    接著建置 CRUD 的介面,我是使用 Products 這張資料表建置,可以參考:

    建置完成後,顯示頁面如下:

    Products Model 內容如下,建置出來就已經有 virtual :
    public partial class Products
    {
    
        public Products()
        {
            this.Order_Details = new HashSet<Order_Details>();
    
        }
    
        public int ProductID { get; set; }
        public string ProductName { get; set; }
        public Nullable<int> SupplierID { get; set; }
        public Nullable<int> CategoryID { get; set; }
        public string QuantityPerUnit { get; set; }
        public Nullable<decimal> UnitPrice { get; set; }
        public Nullable<short> UnitsInStock { get; set; }
        public Nullable<short> UnitsOnOrder { get; set; }
        public Nullable<short> ReorderLevel { get; set; }
        public bool Discontinued { get; set; }
    
        public virtual Categories Categories { get; set; }
        public virtual ICollection<Order_Details> Order_Details { get; set; }
        public virtual Suppliers Suppliers { get; set; }
    
    }
    

    在 ProductsController 內 Get 程式碼如下:
    public ActionResult Index()
    {
        var products = db.Products;
        return View(products.ToList());
    }
    

    接著將 Products Model 內的關聯成員的 virtual 關鍵字拿掉後執行畫面如下:

    看出差別了嗎? CategoryName 不會顯示出來,我們回到 View 端看一下它的程式碼:
    @Html.DisplayNameFor(model => model.Categories.CategoryName)
    

    這就是所謂的延遲載入的差異,如果將 virtual 關鍵字拿掉,就使用到此關聯成員時,就不會載入;而若有 virtual 關鍵字,則會載入。在 Controller 端其實都沒有變,而是 virtual 使它變得更靈活。

    如果真的不加也有另外的做法,就是在資料讀取時,就關聯表的資料 Include 進來:
    public ActionResult Index()
    {
        var products = db.Products.Include(x => x.Categories);
        return View(products.ToList());
    }
    


    2014年4月1日 星期二

    ASP.NET MVC 4 遇到「參數 @objname 模稜兩可或是所宣告的 @objtype (COLUMN) 有誤。」 之解決方案

    今天 MVC 在做 code first 自動移轉時遇到「參數 @objname 模稜兩可或是所宣告的 @objtype (COLUMN) 有誤。」,如下圖


    怪了,平常改動資料欄位,也不會產生錯誤,檢查了一下,我的程式碼只是將一對多關聯改成一對一 ( 以下為示意 ):
    public class A
        {
            [Key]
            [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
            public Guid AId { get; set; }
    
            public string Name { get; set; }
    
            // 修改前
            // public ICollection<B> B { get; set; }
    
            // 修改後
            public B B { get; set; }
        }
    
        public class B
        {
            [Key]
            [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
            public Guid BId { get; set; }
    
            // ...
        }
    

    後來上網搜尋了一下, EF CodeFirst: Either the parameter @objname is ambiguous or the claimed @objtype (COLUMN) is wrong 有說明這是因為欄位在 rename 時發生錯誤,這不太可能吧!我應該只是將 B 的 A_AId 轉移到 A 的 B_BId,好像沒有 rename 的動作。

    接著我將它的移轉檔案叫出來,在「套件管理器主控台」使用指令 Add-Migration Test 將檔案產出,這才發現問題:
    public partial class Test : DbMigration
    {
        public override void Up()
        {
            DropForeignKey("dbo.B", "A_AId", "dbo.A");
            DropIndex("dbo.B", new[] { "A_AId" });
            RenameColumn(table: "dbo.A", name: "A_AId", newName: "B_BId");
            AddForeignKey("dbo.A", "B_BId", "dbo.B", "BId");
            CreateIndex("dbo.A", "B_BId");
        }
        
        public override void Down()
        {
            DropIndex("dbo.A", new[] { "B_BId" });
            DropForeignKey("dbo.A", "B_BId", "dbo.B");
            RenameColumn(table: "dbo.A", name: "B_BId", newName: "A_AId");
            CreateIndex("dbo.B", "A_AId");
            AddForeignKey("dbo.B", "A_AId", "dbo.A", "AId");
        }
    }
    

    Up 在 Migration ( 轉移 ) 時,會執行,而如果發生錯誤則會使用 Down RollBack。

    問題就是發生在 Up 的第三行 - RenameColumn( ... ),在 A 資料表並無 B_BId 欄位,但居然要把 B_BId 命名成為 A_AId ? 所以這一看就是有問題,重新執行後問題依舊存在。

    所以建議先把一對多關連註解掉運行 API,再將一對一關聯加入在運行 API 後就會正常,或者你也可以就移轉檔案改動:
    public override void Up()
    {
        DropForeignKey("dbo.B", "A_AId", "dbo.A");
        DropIndex("dbo.B", new[] { "A_AId" });
        DropColumn("dbo.B", "A_AId");
    
        AddColumn("dbo.A", "B_BId", c => c.Guid());
        AddForeignKey("dbo.A", "B_BId", "dbo.B", "BId");
        CreateIndex("dbo.A", "B_BId");
    }
    

    前者解決方法會比自己修改移轉檔案來得好,因為你還是要看程式碼找出錯誤在哪裡,比較花時間。

    2014年3月30日 星期日

    ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- Message Box ( 訊息框 )

    通常使用一般網頁,填妥一張表格要送出,可能會將頁面導回主檔頁面,或者是在頁面上顯示文字說明送出完成,也可以像這篇主題一樣,彈出訊息讓使用者知道動作已經完成,可以按下確定,繼續下一個動作。

    訊息框

    首先就來建立一個基本的訊息框:
    Ext.MessageBox.alert('提示', '儲存成功');
    

    或者,在按下訊息框後再處理另外的指令,可以這樣處理:
    Ext.MessageBox.alert('提示', '儲存成功', callback);
    
    function callback() {
        alert('視窗關閉');
    }
    

    執行後畫面如下:

    確認框

    Ext.MessageBox.confirm("確認","確定儲存");
    

    執行後畫面如下:

    這裡可以結合小弟先前的 ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- 動態複製表單並送出 的文章,再送出時加上確認框,如果確認則送出,取消則不送出。按鈕結果狀態有二, yes 或 no。

    在送出表單時可以修改成:
    Ext.MessageBox.confirm("確認", "確定儲存", function (btn) {
        if (btn == 'yes') {
            Ext.getCmp('form').getForm().submit({
                // 傳送到 url
                url: 'http://localhost:8090/api/Product/Post',
                method: 'POST'
            });
        }
    });
    

    而 API 就會接收到 Form 來的訊息

    修改按鈕顯示文字

    上面的例子在彈出視窗後,按鈕文字都是英文的,如何修改按鈕顯示文字,透過以下指令:
    Ext.MessageBox.buttonText = {
        ok: "確定",
        cancel: "取消",
        yes: "是",
        no: "否"
    };
    

    而確認框中,就會顯示:

    輸入框

    輸入空可以用在密碼再次確認的情況,將密碼送回到 API 檢查,視使用者是否有權限進入下一頁。
    Ext.MessageBox.prompt("輸入", "您的密碼:", function (btn, text) {
       
    });
    

    按鈕結果狀態有二,ok 與 cancel, btn 為選擇狀態;text 為輸入文字。

    自訂義訊息框

    參數設定如以下程式碼,稍微看一下註解應該就差不多了:
    Ext.MessageBox.show({
        title: '標題',
        msg: "自訂義訊息框",
        // 寬度
        width: 300,
        // 多行輸入
        multiline: true,
        // 關閉按鈕
        closable: false,
        // 顯示圖示
        icon: Ext.MessageBox.INFO,
        // 按鈕型態
        buttons: Ext.MessageBox.YESNOCANCEL,
    
        fn:  function(btn, text) {
       
        }
    });
    

    icon 可以設定成: INFO ( 訊息圖示 )、WARNING ( 警告圖示 )、QUESTION ( 詢問圖示 )、 ERROR ( 錯誤圖示 )
    buttons 可以設定成: OK ( 確定 )、 CANCEL ( 取消 )、OKCANCEL( 確定和取消 )、YESNO ( 是和否 )、YESNOCANCEL ( 是和否和取消 )

    進度框

    進度框也是自訂義訊息框的一種,它只需要把屬性 progress 改為 true 就可以了。

    進度框可以使用 Ext.MessageBox.updateProgress 設定進度與文字,以下參考 ExtJs学习之弹出框,提示框,输入框等框

    Ext.MessageBox.show({
        title: '上傳中',
        msg: "正在上傳檔案...",
        progressText: '正在初始化...',
        // 寬度
        width: 300,
        // 關閉按鈕
        closable: false,
        // 顯示進度
        progress: true,
        fn:  function(btn,text) {   
        }
    
    });
    
    var f = function (v) {
        return function () {
            if (v == 22) {
                Ext.MessageBox.hide();
                Ext.MessageBox.alert('完成', '所有項目上傳完成');
            } else {
                var i = v / 21;
                Ext.MessageBox.updateProgress(i, Math.round(100 * i) + '% 已完成');
            }
        };
    };
    
    for (var i = 0; i < 23; i++) {
        setTimeout(f(i), i * 500);
    }
    

    執行後如下圖:

    參考與引用:ExtJs学习之弹出框,提示框,输入框等框ExtJS 入门学习之 messagebox篇

    2014年3月26日 星期三

    中文數字轉阿拉伯數字 ( C# 測試版 )

    如何將「六十兆零五十二億三千八百六十二萬六千四百二十五」轉為阿拉伯數字,網路上大部分都是將阿拉伯數字轉為中文數字,所以我就自己來寫一個中文數字轉阿拉伯數字。

    首先要定義一、二、三、....、九的對應,且要定義百、千、億、兆的對應,由於「兆」已經超過 int ( 整數 ) 範圍了,所以在這裡使用 log ( 長整數 ):
    Dictionary<string, long> digit =
        new Dictionary<string, long>() 
        { { "一", 1 }, 
          { "二", 2 }, 
          { "三", 3 }, 
          { "四", 4 }, 
          { "五", 5 }, 
          { "六", 6 }, 
          { "七", 7 }, 
          { "八", 8 }, 
          { "九", 9 } };
    Dictionary<string, long> word =
        new Dictionary<string, long>() 
        { { "百", 100 }, 
          { "千", 1000 }, 
          { "萬", 10000 }, 
          { "億", 100000000 }, 
          { "兆", 1000000000000 } };
    
    Dictionary<string, long> ten =
        new Dictionary<string, long>() 
        { { "十", 10 } }; 
    

    為何要這樣宣告?假設說你的一為壹、二為貳、.....、九為玖,就可以多加幾列
    { "壹", 1 }, 
    { "貳", 2 }, 
    
    ...
    
    { "玖", 9 },
    

    百、千、億、兆 位數字也可以這樣子做,因為中文數字可以混用,甚至可以使用簡體中文,達到最大使用性。

    接下來就是程式部份了,其實規格很簡單,數字由左向右,或加或乘且紀錄,當文字碰到比千還大的文字 ( 萬、億、兆 ),就要將紀錄值

    我使用兩個變數儲存 t_l、 _t_l,_t_l 紀錄該次讀取的文字,先將文字除掉所有零字。

    若為數字,則紀錄於 _t_l;若為十,則看 _t_l 是否為 0,不為 0,則 _t_l 乘上 10,為 0,則 _t_l 為 10;
    若不為數字,則將 _t_l 乘上 文字對應的值;
    若碰上比千位數大的文字 ( 萬、億、兆 ),就要將 _t_l 加上 t_l 再乘文字對應的值。

    最後再把殘餘值加上,輸出結果。

    先看程式碼:
    public long GetChineseNumberToInt(string s)
    {
        long iResult = 0;
    
        s = s.Replace("零", "");
        int index = 0;
        long t_l = 0, _t_l = 0;
        string t_s;
    
        while (s.Length > index)
        {
            t_s = s.Substring(index++, 1);
    
            // 數字
            if (digit.ContainsKey(t_s))
            {
                _t_l += digit[t_s];
            }
            // 十
            else if (ten.ContainsKey(t_s))
            {
                _t_l = _t_l == 0 ? 10 : _t_l * 10;
            }
            // 百、千、億、兆 
            else if (word.ContainsKey(t_s))
            {
                // 碰到千位則使 _t_l 與 t_l 相加乘上目前讀到的數字,
                // 並將輸出結果累加。
                if (word[t_s] > word["千"])
                {
                    iResult += (t_l + _t_l) * word[t_s];
                    t_l = 0;
                    _t_l = 0;
    
                    continue;
                }
                _t_l = _t_l * word[t_s];
                t_l += _t_l;
    
                _t_l = 0;
            }
    
    
        }
        // 將殘餘值累加至輸出結果
        iResult += t_l;
        iResult += _t_l;
    
        return iResult;
    
    }
    

    迴圈每走完一次,數值的變化:
    文字iResultt_l_t_l
    006
    0060
    6000000000000000
    6000000000000005
    60000000000000050
    60000000000000052
    6000520000000000
    6000520000000003
    6000520000000030000
    6000520000000030008
    6000520000000038000
    6000520000000038006
    60005200000000380060
    60005200000000380062
    6000523862000000
    6000523862000006
    6000523862000060000
    6000523862000060004
    6000523862000064000
    6000523862000064002
    60005238620000640020
    60005238620000640025

    這樣看數值變化比較有感覺。

    最後把殘餘值加上,輸出 60005238626425。




    2014年3月24日 星期一

    ASP.NET MVC 4 WebApi -- 利用繼承實作多專案間的部分類別(partial)

    多人開發的專案裡,部分類別(partial)是非常好用的東西,但有個小小的缺點,只能再同一個exe或dll才有作用,如果是參考其他專案的dll,編譯就會失敗
    為了減少相同程式碼出現的頻率,所以改用繼承的方式

    A專案
    namespace A.Models
    {
        public class Field
        {
            public Guid FieldId { get; set; }
            public DateTime CreateOn { get; set; }
            public Guid Creater { get; set; }
            public DateTime UpdateOn { get; set; }
            public Guid Updater { get; set; }
      
            public ICollection Language { get; set; }
        }
    
        public class FieldLanguage
        {
            public Guid FieldLanguageId { get; set; }
            public string Aliases { get; set; }
            public string Notes { get; set; }
            public string Language { get; set; }
      
            public Field Field { get; set; }
        }
    }
    

    B專案(參考A專案)
    namespace B.Models
    {
        public class Field : A.Models.Field
        {
      //放置View所需語系資料
            public string Language_Aliases { get; set; }
            public string Language_Notes { get; set; }
        }
    }
    

    當需要把FieldLanguage放到變數中做處理時,就會發生"找不到類型或命名空間名稱'FieldLanguage'"的錯誤

    B的Model追加FieldLanguage,因為欄位相同,所以一樣繼承A的FieldLanguage
    public class FieldLanguage : A.Models.FieldLanguage { }
    

    雖然解決了找不到類別的問題,但是也多了"類型'A.Models.FieldLanguage'不能隱含轉換為'B.Models.FieldLanguage'"的錯誤
    原來繼承裡面所關聯的類別是原本所指定的,不會因為繼承的專案有相同類型名稱而轉換過去,所以繼承後要再把關聯加回去
    public ICollection FieldLanguage { get; set; }
    

    這邊會有一個警告,"'B.Models.FieldLanguage'隱藏了繼承的成員'A.Models.FieldLanguage'。如果是刻意要隱藏,請使用new關鍵字"
    因為名稱是一樣的,所以編譯器會自動把父類別的屬性或方法隱藏掉
    PS:範例是用屬性,所以用new的方式,如果是方法,請盡量使用override(可參考new和override的差異與目的)
    public new ICollection FieldLanguage { get; set; }
    

    FieldLanguage當然也要參考回Field
    public new Field Field { get; set; }
    

    最後B的類別如下:
    namespace B.Models
    {
        public class Field : A.Models.Field
        {
            public string Language_Aliases { get; set; }
    
            public string Language_Notes { get; set; }
    
            public new ICollection FieldLanguage { get; set; }
        }
    
        public class FieldLanguage : A.Models.FieldLanguage
        {
            public new Field Field { get; set; }
        }
    }
    
    以上就可以完成多專案的部分類別,各專案本身當然也是可以繼續使用部分類別(partial)

    問:為什麼要加回關聯?
    答:因為在Model只要做一次,後面資料處理就可以很輕鬆,當然不加也是可以,只是資料處理還要做型態轉換,如果這個Model很多地方會使用,那光型態轉換就有得忙了

    參考:
    new和override的差異與目的
    區分 abstract、virtual、override 和 new

    2014年3月22日 星期六

    ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- Tabs ( 頁籤 )

    頁籤 ( Tabs ) 在許多網頁中都可以看到它的使用,通常使用時機就是這個頁面資訊太多,但又必須讓使用者不需要做太多的動作而得知資訊,如果一次將資訊顯示出來,那使用者必須滾動滑鼠滾輪才能看到資訊,通常最主要的資訊是不會讓使用者做這個動作才能看到,所以頁籤的使用時機就是在此,例如: yahoo 首頁的新聞,我最常使用的就是這一塊,我只需要幾個點擊就可以觀看到主要的新聞,算是一個很方便且實用的控制項。

    基本

    先來 View 端如何建置 Tabs,以下為最基本的程式碼,item 內為頁籤與其內容或方法:
    var tp = new Ext.TabPanel({
        renderTo: Ext.getBody(),
        width: 600,
        height: 200,
        items: [
            
        ]
    });
    

    預設頁面載入時停留在某一個 tab,與陣列索引值相同,第一個為 0:
    activeTab: 0
    

    頁籤顯示位置,可以設置 left、top、right、bottom
    tabPosition: 'left'
    

    一般頁籤

    先加入一個頁籤:
    {
        title: '一般頁籤',
        html: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Sed metus nibh, sodales a, porta at, vulputate eget, dui. Pellentesque ut nisl. Maecenas tortor turpis, interdum non, sodales non, iaculis ac, lacus. ' +
              'Vestibulum auctor, tortor quis iaculis malesuada, libero lectus bibendum purus, sit amet tincidunt quam turpis vel lacus. In pellentesque nisl non sem. Suspendisse nunc sem, pretium eget, cursus a, fringilla vel, urna.'
    },
    

    一般內容設置

    上面的頁籤內容相當的不好看,且內容好像無法顯示完全,我們要調整一下內容的顯示:
    bodyPadding: 15 // 內縮 15 單位
    autoScroll: true // 內容超過高度,則顯示滾輪軸
    

    autoHeight: true // 自動調整高度,前提是不能設置 Ext.TabPanel 的高度
    

    closable: true // 顯示關閉按鈕
    

    Ajax 頁籤

    使用 API 讀取頁籤內容,首先先準備 API 回傳內容:
    public ContentResult AjaxContent()
    {
        return Content("從 api 來的資料。");
    }
    

    而在 View 端則需要這樣寫:
    {
        title: 'ajax 頁籤',
        loader: {
            url: 'http://localhost:8090/Ext/AjaxContent'
        },
        listeners: {
            activate: function (tab) {
                tab.loader.load();
            }
        }
    },
    

    activate 事件代表點擊到此頁籤時執行。

    上述使用 listeners 的這個方法也可以在 loader 內使用 autoLoad: true 有相同效果,差別就在於一個是點擊後才會載入,一個是在頁面載入時就會一同載入。

    執行後畫面如下:

    設置在資料未從 API 載入前先顯示 Loading 圖示:
    loadMask: true
    contentType: 'html' // 格式
    

    事件頁籤

    其實上面已經提過了,這裡就只顯示程式碼:
    {
        title: '事件頁籤',
        listeners: {
            activate: function (tab) {
                setTimeout(function () {
                    alert(tab.title + ' 已使用.');
                }, 1);
            },
            deactivate: function (tab) {
                setTimeout(function () {
                    alert(tab.title + ' 已離開e.');
                }, 1);
            }
        },
        html: "不管此頁籤使用或離開,都會觸發事件"
    },
    

    失效頁籤

    使頁籤不能點擊與使用
    {
        title: '失效頁籤',
        disabled: true,
        html: "Can't see me cause I'm disabled"
    }
    




    2014年3月19日 星期三

    Extjs 使用 loadData 會對應不到 mapping 的值之解決方法

    我先前有針對 Combo 連動作一篇解說:ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- Combo 連動

    我在做四層 Combo 資料連動時發現了這個問題,一般連動選單項目時,如果顯示的內容和值在同一層 json 裡面不會有問題,這個問題發生在如果顯示的內容與值是在內嵌欄位時,資料會對應錯誤而導致顯示不出來

    先看原本從 API 傳過來的資料 ( 已經過刪減,註解部分為後來加上 ):
    "JobLevel": [
        "Id": "1",
        "Code": "T1", 
        {
            "JobGroupMapping": [ 
                {
                    "Id": "1",
        
                    ...
                    // 顯示資料
                    "JobGroup": {
                        "Id": "1",
                        "Language_Name": "工程師" 
                    },
                    // 連動資料
                    "JobTitle": [
                            {
                                "Id": "1",
                      
                                // ...
                      
                                "Language_Name": "工程師"
                            },
                            {
                                "Id": "2",
          
                                // ...
          
                                "Language_Name": "資深工程師"
                            },
                            {
                                "Id": "3",
          
                                 // ...
          
                                "Language_Name": "主任工程師"
                            }
                    ]
                }
            ]
        }
    ]
    

    而我這兩張表的 Extjs Model 為:
    Ext.define('JobLevel', {
        extend: 'Ext.data.Model',
        fields: [
            { name: 'Id', type: 'string' },
            { name: 'Language_Name', type: 'string' }
        ],
        hasMany: { model: 'JobGroupMapping', name: 'JobGroupMapping' }
    });
    
    Ext.define('JobGroupMapping', {
        extend: 'Ext.data.Model',
        fields: [
            { name: 'Id', type: 'string' },
            { name: 'JobGroupId', type: 'string', mapping: 'JobGroup.Id' },
            { name: 'Language_Name', type: 'string', mapping: 'JobGroup.Language_Name' }
        ],
        belongsTo: 'JobLevel'
    });
    

    mapping 的意思就是將內嵌 json 的值拉到同一層對應成另外名稱,有別名的意味。選項選取時,就必須設定連動選項的資料內容,這裡我是使用 loadData 將下一層資料帶入 :
    {
        itemId: 'field_joblevel',
        name: 'JobLevel.Id',
        editable: false,
        store: storeJobLevel,
        xtype: 'combobox',
        fieldLabel: 'JobLevel',
        queryMode: 'local',
        valueField: 'Id',
        displayField: 'Code',
        listeners: {
            change: function (combo, aNew, aOld) {
                var record = this.findRecord(this.valueField, aNew);
                JobGroupMapping.loadRawData(record.raw.JobGroupMapping);
                var tCombo = this.up('form').getComponent('field_jobgroup');
                tCombo.setValue(tCombo.getStore().getAt(0).get(tCombo.valueField));
            }
        }
    },
    {
        itemId: 'field_jobgroup',
        name: 'JobGroup.Id',
        editable: false,
        store: JobGroupMapping,
        xtype: 'combobox',
        fieldLabel: 'JobGroup',
        queryMode: 'local',
        valueField: 'Id',
        displayField: 'Language_Name'
    }
    

    此時問題發生了,我在選擇 JobLevel 的這個選項,接著會對應不到 JobGroup 這個選項。

    後來我研究與參考了一下網路上的資料,發現有另外一個方法可以解決這問題 -- loadRawData,比較一下這兩個方法,此時才了解連動時必須要注意此情況:

    loadRawData

    Loads data via the bound Proxy's reader
    Use this method if you are attempting to load data and want to utilize the configured data reader

    loadData

    Loads an array of data straight into the Store.

    Using this method is great if the data is in the correct format already (e.g. it doesn't need to be processed by a reader). If your data requires processing to decode the data structure, use a MemoryProxy or loadRawData.

    差異就是 loadRawData 會將資料結構解析,所以沒有問題,但使用 loadData 的前提之下必須要資料都在同一層,否則必須要去使用 MemoryProxy 或者 loadRawData。

    參考:Extjs store load json data,store fields mapping can't show dataExt.data.StoreView

    2014年3月16日 星期日

    ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- 兩層複選群組 ( 2 Level CheckBoxGroup )

    此文章是 ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- 複選群組 ( CheckBoxGroup ) 做功能上的延伸,兩層代表多層的意思,這裡繼續延伸成縣市地區選擇後,郵遞區號自動帶出,這種功能在許多地方都看得到。

    XML 調整

    上一篇文章的 XML 資料來源要加上一些細項資料:
    <?xml version="1.0" encoding="utf-8" ?>
    <zip>
      <city name="基隆市">
        <area name="仁愛區" zipcode="200"></area>
        <area name="信義區" zipcode="201"></area>
        <area name="中正區" zipcode="202"></area>
        <area name="中山區" zipcode="203"></area>
        <area name="安樂區" zipcode="204"></area>
        <area name="暖暖區" zipcode="205"></area>
        <area name="七堵區" zipcode="206"></area>
      </city>
      <city name="台北市">
        <area name="中正區" zipcode="100"></area>
        <area name="大同區" zipcode="103"></area>
        <area name="中山區" zipcode="104"></area>
        <area name="松山區" zipcode="105"></area>
        <area name="大安區" zipcode="106"></area>
        <area name="萬華區" zipcode="108"></area>
        <area name="信義區" zipcode="110"></area>
        <area name="士林區" zipcode="111"></area>
        <area name="北投區" zipcode="112"></area>
        <area name="內湖區" zipcode="114"></area>
        <area name="南港區" zipcode="115"></area>
        <area name="文山區" zipcode="116"></area>
      </city>
      <city name="新北市">
        <area name="萬里區" zipcode="207"></area>
        <area name="金山區" zipcode="208"></area>
        <area name="板橋區" zipcode="220"></area>
        <area name="汐止區" zipcode="221"></area>
        <area name="深坑區" zipcode="222"></area>
        <area name="石碇區" zipcode="223"></area>
        <area name="瑞芳區" zipcode="224"></area>
        <area name="平溪區" zipcode="226"></area>
        <area name="雙溪區" zipcode="227"></area>
        <area name="貢寮區" zipcode="228"></area>
        <area name="新店區" zipcode="231"></area>
        <area name="坪林區" zipcode="232"></area>
        <area name="烏來區" zipcode="233"></area>
        <area name="永和區" zipcode="234"></area>
        <area name="中和區" zipcode="235"></area>
        <area name="土城區" zipcode="236"></area>
        <area name="三峽區" zipcode="237"></area>
        <area name="樹林區" zipcode="238"></area>
        <area name="鶯歌區" zipcode="239"></area>
        <area name="三重區" zipcode="241"></area>
        <area name="新莊區" zipcode="242"></area>
        <area name="泰山區" zipcode="243"></area>
        <area name="林口區" zipcode="244"></area>
        <area name="蘆洲區" zipcode="247"></area>
        <area name="五股區" zipcode="248"></area>
        <area name="八里區" zipcode="249"></area>
        <area name="淡水區" zipcode="251"></area>
        <area name="三芝區" zipcode="252"></area>
        <area name="石門區" zipcode="253"></area>
      </city>
      <city name="桃園縣">
        <area name="中壢市" zipcode="320"></area>
        <area name="平鎮市" zipcode="324"></area>
        <area name="龍潭鄉" zipcode="325"></area>
        <area name="楊梅市" zipcode="326"></area>
        <area name="新屋鄉" zipcode="327"></area>
        <area name="觀音鄉" zipcode="328"></area>
        <area name="桃園市" zipcode="330"></area>
        <area name="龜山鄉" zipcode="333"></area>
        <area name="八德市" zipcode="334"></area>
        <area name="大溪鎮" zipcode="335"></area>
        <area name="復興鄉" zipcode="336"></area>
        <area name="大園鄉" zipcode="337"></area>
        <area name="蘆竹鄉" zipcode="338"></area>
      </city>
      <city name="新竹市">
        <area name="東區" zipcode="300"></area>
        <area name="北區" zipcode="300"></area>
        <area name="香山區" zipcode="300"></area>
      </city>
      <city name="新竹縣">
        <area name="竹北市" zipcode="302"></area>
        <area name="湖口鄉" zipcode="303"></area>
        <area name="新豐鄉" zipcode="304"></area>
        <area name="新埔鎮" zipcode="305"></area>
        <area name="關西鎮" zipcode="306"></area>
        <area name="芎林鄉" zipcode="307"></area>
        <area name="寶山鄉" zipcode="308"></area>
        <area name="竹東鎮" zipcode="310"></area>
        <area name="五峰鄉" zipcode="311"></area>
        <area name="橫山鄉" zipcode="312"></area>
        <area name="尖石鄉" zipcode="313"></area>
        <area name="北埔鄉" zipcode="314"></area>
        <area name="峨眉鄉" zipcode="315"></area>
      </city>
      <city name="苗栗縣">
        <area name="竹南鎮" zipcode="350"></area>
        <area name="頭份鎮" zipcode="351"></area>
        <area name="三灣鄉" zipcode="352"></area>
        <area name="南庄鄉" zipcode="353"></area>
        <area name="獅潭鄉" zipcode="354"></area>
        <area name="後龍鎮" zipcode="356"></area>
        <area name="通霄鎮" zipcode="357"></area>
        <area name="苑裡鎮" zipcode="358"></area>
        <area name="苗栗市" zipcode="360"></area>
        <area name="造橋鄉" zipcode="361"></area>
        <area name="頭屋鄉" zipcode="362"></area>
        <area name="公館鄉" zipcode="363"></area>
        <area name="大湖鄉" zipcode="364"></area>
        <area name="泰安鄉" zipcode="365"></area>
        <area name="銅鑼鄉" zipcode="366"></area>
        <area name="三義鄉" zipcode="367"></area>
        <area name="西湖鄉" zipcode="368"></area>
        <area name="卓蘭鎮" zipcode="369"></area>
      </city>
      <city name="台中市">
        <area name="中區" zipcode="400"></area>
        <area name="東區" zipcode="401"></area>
        <area name="南區" zipcode="402"></area>
        <area name="西區" zipcode="403"></area>
        <area name="北區" zipcode="404"></area>
        <area name="北屯區" zipcode="406"></area>
        <area name="西屯區" zipcode="407"></area>
        <area name="南屯區" zipcode="408"></area>
        <area name="太平區" zipcode="411"></area>
        <area name="大里區" zipcode="412"></area>
        <area name="霧峰區" zipcode="413"></area>
        <area name="烏日區" zipcode="414"></area>
        <area name="豐原區" zipcode="420"></area>
        <area name="后里區" zipcode="421"></area>
        <area name="石岡區" zipcode="422"></area>
        <area name="東勢區" zipcode="423"></area>
        <area name="和平區" zipcode="424"></area>
        <area name="新社區" zipcode="426"></area>
        <area name="潭子區" zipcode="427"></area>
        <area name="大雅區" zipcode="428"></area>
        <area name="神岡區" zipcode="429"></area>
        <area name="大肚區" zipcode="432"></area>
        <area name="沙鹿區" zipcode="433"></area>
        <area name="龍井區" zipcode="434"></area>
        <area name="梧棲區" zipcode="435"></area>
        <area name="清水區" zipcode="436"></area>
        <area name="大甲區" zipcode="437"></area>
        <area name="外埔區" zipcode="438"></area>
        <area name="大安區" zipcode="439"></area>
      </city>
      <city name="彰化縣">
        <area name="彰化市" zipcode="500"></area>
        <area name="芬園鄉" zipcode="502"></area>
        <area name="花壇鄉" zipcode="503"></area>
        <area name="秀水鄉" zipcode="504"></area>
        <area name="鹿港鎮" zipcode="505"></area>
        <area name="福興鄉" zipcode="506"></area>
        <area name="線西鄉" zipcode="507"></area>
        <area name="和美鎮" zipcode="508"></area>
        <area name="伸港鄉" zipcode="509"></area>
        <area name="員林鎮" zipcode="510"></area>
        <area name="社頭鄉" zipcode="511"></area>
        <area name="永靖鄉" zipcode="512"></area>
        <area name="埔心鄉" zipcode="513"></area>
        <area name="溪湖鎮" zipcode="514"></area>
        <area name="大村鄉" zipcode="515"></area>
        <area name="埔鹽鄉" zipcode="516"></area>
        <area name="田中鎮" zipcode="520"></area>
        <area name="北斗鎮" zipcode="521"></area>
        <area name="田尾鄉" zipcode="522"></area>
        <area name="埤頭鄉" zipcode="523"></area>
        <area name="溪州鄉" zipcode="524"></area>
        <area name="竹塘鄉" zipcode="525"></area>
        <area name="二林鎮" zipcode="526"></area>
        <area name="大城鄉" zipcode="527"></area>
        <area name="芳苑鄉" zipcode="528"></area>
        <area name="二水鄉" zipcode="530"></area>
      </city>
      <city name="南投縣">
        <area name="南投市" zipcode="540"></area>
        <area name="中寮鄉" zipcode="541"></area>
        <area name="草屯鎮" zipcode="542"></area>
        <area name="國姓鄉" zipcode="544"></area>
        <area name="埔里鎮" zipcode="545"></area>
        <area name="仁愛鄉" zipcode="546"></area>
        <area name="名間鄉" zipcode="551"></area>
        <area name="集集鎮" zipcode="552"></area>
        <area name="水里鄉" zipcode="553"></area>
        <area name="魚池鄉" zipcode="555"></area>
        <area name="信義鄉" zipcode="556"></area>
        <area name="竹山鎮" zipcode="557"></area>
        <area name="鹿谷鄉" zipcode="558"></area>
      </city>
      <city name="雲林縣">
        <area name="斗南鎮" zipcode="630"></area>
        <area name="大埤鄉" zipcode="631"></area>
        <area name="虎尾鎮" zipcode="632"></area>
        <area name="土庫鎮" zipcode="633"></area>
        <area name="褒忠鄉" zipcode="634"></area>
        <area name="東勢鄉" zipcode="635"></area>
        <area name="台西鄉" zipcode="636"></area>
        <area name="崙背鄉" zipcode="637"></area>
        <area name="麥寮鄉" zipcode="638"></area>
        <area name="斗六市" zipcode="640"></area>
        <area name="林內鄉" zipcode="643"></area>
        <area name="古坑鄉" zipcode="646"></area>
        <area name="莿桐鄉" zipcode="647"></area>
        <area name="西螺鎮" zipcode="648"></area>
        <area name="二崙鄉" zipcode="649"></area>
        <area name="北港鎮" zipcode="651"></area>
        <area name="水林鄉" zipcode="652"></area>
        <area name="口湖鄉" zipcode="653"></area>
        <area name="四湖鄉" zipcode="654"></area>
        <area name="元長鄉" zipcode="655"></area>
      </city>
      <city name="嘉義市">
        <area name="東區" zipcode="600"></area>
        <area name="西區" zipcode="600"></area>
      </city>
      <city name="嘉義縣">
        <area name="番路鄉" zipcode="602"></area>
        <area name="梅山鄉" zipcode="603"></area>
        <area name="竹崎鄉" zipcode="604"></area>
        <area name="阿里山鄉" zipcode="605"></area>
        <area name="中埔鄉" zipcode="606"></area>
        <area name="大埔鄉" zipcode="607"></area>
        <area name="水上鄉" zipcode="608"></area>
        <area name="鹿草鄉" zipcode="611"></area>
        <area name="太保市" zipcode="612"></area>
        <area name="朴子市" zipcode="613"></area>
        <area name="東石鄉" zipcode="614"></area>
        <area name="六腳鄉" zipcode="615"></area>
        <area name="新港鄉" zipcode="616"></area>
        <area name="民雄鄉" zipcode="621"></area>
        <area name="大林鎮" zipcode="622"></area>
        <area name="溪口鄉" zipcode="623"></area>
        <area name="義竹鄉" zipcode="624"></area>
        <area name="布袋鎮" zipcode="625"></area>
      </city>
      <city name="台南市">
        <area name="中西區" zipcode="700"></area>
        <area name="東區" zipcode="701"></area>
        <area name="南區" zipcode="702"></area>
        <area name="北區" zipcode="704"></area>
        <area name="安平區" zipcode="708"></area>
        <area name="安南區" zipcode="709"></area>
        <area name="永康區" zipcode="710"></area>
        <area name="歸仁區" zipcode="711"></area>
        <area name="新化區" zipcode="712"></area>
        <area name="左鎮區" zipcode="713"></area>
        <area name="玉井區" zipcode="714"></area>
        <area name="楠西區" zipcode="715"></area>
        <area name="南化區" zipcode="716"></area>
        <area name="仁德區" zipcode="717"></area>
        <area name="關廟區" zipcode="718"></area>
        <area name="龍崎區" zipcode="719"></area>
        <area name="官田區" zipcode="720"></area>
        <area name="麻豆區" zipcode="721"></area>
        <area name="佳里區" zipcode="722"></area>
        <area name="西港區" zipcode="723"></area>
        <area name="七股區" zipcode="724"></area>
        <area name="將軍區" zipcode="725"></area>
        <area name="學甲區" zipcode="726"></area>
        <area name="北門區" zipcode="727"></area>
        <area name="新營區" zipcode="730"></area>
        <area name="後壁區" zipcode="731"></area>
        <area name="白河區" zipcode="732"></area>
        <area name="東山區" zipcode="733"></area>
        <area name="六甲區" zipcode="734"></area>
        <area name="下營區" zipcode="735"></area>
        <area name="柳營區" zipcode="736"></area>
        <area name="鹽水區" zipcode="737"></area>
        <area name="善化區" zipcode="741"></area>
        <area name="大內區" zipcode="742"></area>
        <area name="山上區" zipcode="743"></area>
        <area name="新市區" zipcode="744"></area>
        <area name="安定區" zipcode="745"></area>
      </city>
      <city name="高雄市">
        <area name="新興區" zipcode="800"></area>
        <area name="前金區" zipcode="801"></area>
        <area name="苓雅區" zipcode="802"></area>
        <area name="鹽埕區" zipcode="803"></area>
        <area name="鼓山區" zipcode="804"></area>
        <area name="旗津區" zipcode="805"></area>
        <area name="前鎮區" zipcode="806"></area>
        <area name="三民區" zipcode="807"></area>
        <area name="楠梓區" zipcode="811"></area>
        <area name="小港區" zipcode="812"></area>
        <area name="左營區" zipcode="813"></area>
        <area name="仁武區" zipcode="814"></area>
        <area name="大社區" zipcode="815"></area>
        <area name="岡山區" zipcode="820"></area>
        <area name="路竹區" zipcode="821"></area>
        <area name="阿蓮區" zipcode="822"></area>
        <area name="田寮區" zipcode="823"></area>
        <area name="燕巢區" zipcode="824"></area>
        <area name="橋頭區" zipcode="825"></area>
        <area name="梓官區" zipcode="826"></area>
        <area name="彌陀區" zipcode="827"></area>
        <area name="永安區" zipcode="828"></area>
        <area name="湖內區" zipcode="829"></area>
        <area name="鳳山區" zipcode="830"></area>
        <area name="大寮區" zipcode="831"></area>
        <area name="林園區" zipcode="832"></area>
        <area name="鳥松區" zipcode="833"></area>
        <area name="大樹區" zipcode="840"></area>
        <area name="旗山區" zipcode="842"></area>
        <area name="美濃區" zipcode="843"></area>
        <area name="六龜區" zipcode="844"></area>
        <area name="內門區" zipcode="845"></area>
        <area name="杉林區" zipcode="846"></area>
        <area name="甲仙區" zipcode="847"></area>
        <area name="桃源區" zipcode="848"></area>
        <area name="那瑪夏區" zipcode="849"></area>
        <area name="茂林區" zipcode="851"></area>
        <area name="茄萣區" zipcode="852"></area>
      </city>
      <city name="屏東縣">
        <area name="屏東市" zipcode="900"></area>
        <area name="三地門鄉" zipcode="901"></area>
        <area name="霧台鄉" zipcode="902"></area>
        <area name="瑪家鄉" zipcode="903"></area>
        <area name="九如鄉" zipcode="904"></area>
        <area name="里港鄉" zipcode="905"></area>
        <area name="高樹鄉" zipcode="906"></area>
        <area name="鹽埔鄉" zipcode="907"></area>
        <area name="長治鄉" zipcode="908"></area>
        <area name="麟洛鄉" zipcode="909"></area>
        <area name="竹田鄉" zipcode="911"></area>
        <area name="內埔鄉" zipcode="912"></area>
        <area name="萬丹鄉" zipcode="913"></area>
        <area name="潮州鎮" zipcode="920"></area>
        <area name="泰武鄉" zipcode="921"></area>
        <area name="來義鄉" zipcode="922"></area>
        <area name="萬巒鄉" zipcode="923"></area>
        <area name="崁頂鄉" zipcode="924"></area>
        <area name="新埤鄉" zipcode="925"></area>
        <area name="南州鄉" zipcode="926"></area>
        <area name="林邊鄉" zipcode="927"></area>
        <area name="東港鎮" zipcode="928"></area>
        <area name="琉球鄉" zipcode="929"></area>
        <area name="佳冬鄉" zipcode="931"></area>
        <area name="新園鄉" zipcode="932"></area>
        <area name="枋寮鄉" zipcode="940"></area>
        <area name="枋山鄉" zipcode="941"></area>
        <area name="春日鄉" zipcode="942"></area>
        <area name="獅子鄉" zipcode="943"></area>
        <area name="車城鄉" zipcode="944"></area>
        <area name="牡丹鄉" zipcode="945"></area>
        <area name="恆春鎮" zipcode="946"></area>
        <area name="滿州鄉" zipcode="947"></area>
      </city>
      <city name="台東縣">
        <area name="台東市" zipcode="950"></area>
        <area name="綠島鄉" zipcode="951"></area>
        <area name="蘭嶼鄉" zipcode="952"></area>
        <area name="延平鄉" zipcode="953"></area>
        <area name="卑南鄉" zipcode="954"></area>
        <area name="鹿野鄉" zipcode="955"></area>
        <area name="關山鎮" zipcode="956"></area>
        <area name="海端鄉" zipcode="957"></area>
        <area name="池上鄉" zipcode="958"></area>
        <area name="東河鄉" zipcode="959"></area>
        <area name="成功鎮" zipcode="961"></area>
        <area name="長濱鄉" zipcode="962"></area>
        <area name="太麻里鄉" zipcode="963"></area>
        <area name="金峰鄉" zipcode="964"></area>
        <area name="大武鄉" zipcode="965"></area>
        <area name="達仁鄉" zipcode="966"></area>
      </city>
      <city name="花蓮縣">
        <area name="花蓮市" zipcode="970"></area>
        <area name="新城鄉" zipcode="971"></area>
        <area name="秀林鄉" zipcode="972"></area>
        <area name="吉安鄉" zipcode="973"></area>
        <area name="壽豐鄉" zipcode="974"></area>
        <area name="鳳林鎮" zipcode="975"></area>
        <area name="光復鄉" zipcode="976"></area>
        <area name="豐濱鄉" zipcode="977"></area>
        <area name="瑞穗鄉" zipcode="978"></area>
        <area name="萬榮鄉" zipcode="979"></area>
        <area name="玉里鎮" zipcode="981"></area>
        <area name="卓溪鄉" zipcode="982"></area>
      </city>
      <city name="宜蘭縣">
        <area name="宜蘭市" zipcode="260"></area>
        <area name="頭城鎮" zipcode="261"></area>
        <area name="礁溪鄉" zipcode="262"></area>
        <area name="壯圍鄉" zipcode="263"></area>
        <area name="員山鄉" zipcode="264"></area>
        <area name="羅東鎮" zipcode="265"></area>
        <area name="三星鄉" zipcode="266"></area>
        <area name="大同鄉" zipcode="267"></area>
        <area name="五結鄉" zipcode="268"></area>
        <area name="冬山鄉" zipcode="269"></area>
        <area name="蘇澳鎮" zipcode="270"></area>
        <area name="南澳鄉" zipcode="272"></area>
      </city>
      <city name="澎湖縣">
        <area name="馬公市" zipcode="880"></area>
        <area name="西嶼鄉" zipcode="881"></area>
        <area name="望安鄉" zipcode="882"></area>
        <area name="七美鄉" zipcode="883"></area>
        <area name="白沙鄉" zipcode="884"></area>
        <area name="湖西鄉" zipcode="885"></area>
      </city>
      <city name="金門縣">
        <area name="金沙鎮" zipcode="890"></area>
        <area name="金湖鎮" zipcode="891"></area>
        <area name="金寧鄉" zipcode="892"></area>
        <area name="金城鎮" zipcode="893"></area>
        <area name="烈嶼鄉" zipcode="894"></area>
        <area name="烏坵鄉" zipcode="896"></area>
      </city>
      <city name="連江縣">
        <area name="南竿鄉" zipcode="209"></area>
        <area name="北竿鄉" zipcode="210"></area>
        <area name="莒光鄉" zipcode="211"></area>
        <area name="東引鄉" zipcode="212"></area>
      </city>
    </zip>
    

    先將來源資料讀取成 array 的函式調整一下:
    var a_zip = [];
    
    $.ajax({
        url: 'http://localhost:8090/Content/xml/zipcode.xml',
        dataType: 'xml',
        error: function () { alert('失敗'); },
        success: function (xml) {
            var cityIndex = 0;
            $(xml).find('city').each(function () {
                var city_name = $(this).attr('name');
    
                var t_area = [];
                var areaIndex = 0;
    
                /* 讀取 city 下 area 資料 */
                $(this).find('area').each(function () {
                    var area_name = $(this).attr('name');
                    var area_zipcode = $(this).attr('zipcode');
    
                    t_area[areaIndex] = {
                        area_name: area_name,
                        area_zipcode: area_zipcode
                    };
    
                    areaIndex ++;
                });
    
                a_zip[cityIndex] = {
                    city_name: city_name,
                    index: cityIndex,
                    /* 將 area 資料放在此 */
                    area: t_area
                };
    
                cityIndex++;
            });
            init();
        }
    });
    

    介面

    在顯示介面上,我將它改為三格,分別顯示郵遞區號、城市、地區
    <input id="targetZipCode" style="width: 35px;" type="text" />
    <input id="targetCity" style="width: 60px;" type="text" />
    <input id="targetArea" style="width: 60px;" type="text" />
    <span id="targetButton"></span>
    


    Button

    此處在修改時必須多設置一個變數 subWindowIsDisplay,判斷子視窗是否在顯示情況。

    在按鈕的 mouseover 事件中,母視窗在顯示時機必須多判斷 subWindowIsDisplay 這個變數,所以整個 Button 的程式碼如下
    var subWindowIsDisplay = false;
    
    var button = Ext.create('Ext.Button', {
        text: '選擇縣市',
        renderTo: 'targetButton',
        listeners: {
            mouseover: {
                fn: function () {
                    if (!this.mousedOver) {
    
                        buttonIsDisplay = true;
                        var tempSelf = this;
                        var tempPosition = tempSelf.getPosition();
                        // X 軸位置
                        tempPosition[0] += 100;
                        // Y 軸位置
                        tempPosition[1] -= 50;
                        locationWinF.setPosition(tempPosition);
                        locationWinF.show();
                    }
                },
                
                // delay: 2000
            },
            mouseout: {
                fn: function () {
                    buttonIsDisplay = false;
                    
                    if (!buttonIsDisplay && !windowIsDisplay && !subWindowIsDisplay) {
                        locationWinS.hide();
                        locationWinF.hide();
                    }
                },
                delay: 500
            },
        }
    });
    

    windows

    在 Windows 的 mouseleave 事件也要稍微做修改
    listeners: {
        afterrender: function(win) {
            win.mon(win.el, {
                mouseover: {
                    fn: function () {
    
                        if (!this.mousedOver) {
                            
                            windowIsDisplay = true;
                            locationWinF.show();
                        }
                    }
                },
                mouseleave: {
                    fn: function () {
                        windowIsDisplay = false;
                        
                        if (!buttonIsDisplay && !windowIsDisplay && !subWindowIsDisplay) {
                            locationWinF.hide();
                            locationWinS.hide();
                        }
                    },
                    delay: 500
                }
            });
        }
    
    },
    

    Windows 針對每一個選項都設置一個 mouseover 的事件,當滑鼠移到選項上,就必須帶出該城市下的地區,剛剛上面程式碼已經有將此轉換成物件了,在 a_zip[tIndex].area 下, locationWinS 為子視窗物件、townMenu 為城市控制項 ID,稍後會提到。


    items: [
        {
            id: 'taiwanCity',
            xtype: 'checkboxgroup',
            fieldLabel: '台灣地區',
            labelAlign: 'top',
            columns: 4,
            vertical: true,
            defaults: {
                width: 150,
                listeners: {
                    afterrender: function () {
                        var tempSelf = this;
                        tempSelf.getEl().on('mouseout', function () {
                            if (!tempSelf.checked) {
                                tempSelf.getEl().setStyle('background-color', '')
                            }
                        });
                        tempSelf.getEl().on('mouseover', function () {
                            tempSelf.getEl().setStyle('background-color', 'cornflowerblue')
                            var tempPosition = tempSelf.getPosition();
    
                            clearTimeout(colock);
                            colock = setTimeout(function () {
                                
                                if (cityValue != tempSelf.inputValue) {
                                    tempPosition[0] += 100;
                                    tempPosition[1] -= 50;
                                    locationWinS.setPosition(tempPosition);
                                    cityValue = tempSelf.inputValue;
                                    var tIndex = cityValue;
                                    Ext.getCmp('townMenu').removeAll();
                                    for (var i = 0; i < a_zip[tIndex].area.length; i++) {
                                        Ext.getCmp('townMenu').add({ boxLabel: a_zip[tIndex].area[i].area_name, name: 'TownId', inputValue: i })
                                    }
                                }
                                locationWinS.show();
                            }, 100)
                        });
                    }
                }
            }
        
        }
    ]
    

    sub windows

    子視窗要做的事情就是在選項被點擊後,將資料帶到文字欄位內。
    var locationWinS = Ext.create('widget.window', {
        header: false,
        style: {
            borderStyle: 'none',
        },
        closable: false,
        closeAction: 'hide',
        items: [
            {
                padding: '10 25 10 25',
                border: false,
                items: [
                    {
                        id: 'townMenu',
                        xtype: 'checkboxgroup',
                        columns: 2,
                        vertical: true,
                        defaults: {
                            width: 110,
                            listeners: {
    
                                afterrender: function () {
                                    var tempSelf = this;
                                    tempSelf.getEl().on('mouseout', function () {
                                        if (!tempSelf.checked) {
                                            tempSelf.getEl().setStyle('background-color', '')
                                        }
    
                                    });
    
                                    tempSelf.getEl().on('mouseleave', function () {
                                        subWindowIsDisplay = false;
                                    });
                                    tempSelf.getEl().on('mouseover', function () {
                                        tempSelf.getEl().setStyle('background-color', 'cornflowerblue')
    
                                        subWindowIsDisplay = true;
                                        
    
                                    }, 500);
    
                                    this.getEl().on('click', function () {
    
                                        $('#targetZipCode').val(
                                            a_zip[cityValue].area[tempSelf.inputValue].area_zipcode
                                            );
                                        $('#targetCity').val(a_zip[cityValue].city_name);
                                        $('#targetArea').val(a_zip[cityValue].area[tempSelf.inputValue].area_name);
    
                                        cityValue = -1;
                                        locationWinF.hide();
                                        locationWinS.hide();
                                        
                                    });
                                }
                            }
                        }
                    }
                ]
            }
        ]
    
    });
    

    最後執行後畫面如下