2011年10月31日

Windows Azure 儲存體–Blob

在Desktop的環境裡面有硬碟可以用來儲存照片、影片、MP3等等,那Windows Azure當然也要儲存體來進行儲存啦,所以這次介紹的是Windows Azure專門用來存取大型資料的儲存體Blob(Binary Large Object),而在談論之前,也有一個觀念要給大家,無論是Blob或是Table、Queue,其實都是存在於資料庫裏面,所以本機要模擬的時候,也必須要有資料庫才能進行測試喔!

Blob架構

既然Blob是儲存在雲端上,就必須符合雲端的特性,所以架構必定和本機上有所不同,以下是Blob的結構圖,我們可以看到,基本上會分成三層,Account(帳號)、Container(容器)、Blob;畢竟網路那麼多使用者,所以一定是用Account來做區分,而一個Account會有非常多的Container,這些Container是可以自己去自訂的,Container的目的其實就像是利用群組的方式,來設定存取的權限,而Container裡面會有許多Blob,基本上就如下圖。( 其實Container也是物件,官方網站也稱Container為Blob Container ),此外,除了下圖外,Container和Blob還擁有Metadata可以進行描述。

image

Storage Account
  • 所有的Blob是透過此帳號進行存取。
  • 他是整個架構的起點。
  • 一個帳號可以有非常多的Container。
Blog Container
  • 此Container必須在Account內,且此Container幫Blob分了群組。
  • 共享權限的等級,是設在Container裡面,目前支援Public Read和Private,當此權限為Public Read時,任何人都可以進行讀取,而不需要驗證,只有驗證過的對應使用者能進行存取。
  • 容器也有Metadata,大小為8k,是一組<name,value>的形式。
  • Blob Containet擁有可以列出所有Blob的方法,所以可以很輕鬆地去做存取。

Blob

  • Blob儲存在Container裡面,而且每個Blob最高可儲存50G!
  • 每個在Container裡面的Blob都有一個獨一無二的字串名稱。
  • Blob也擁有Metadata可以設定,一樣是8k,也是一組<name,value>的形式。

最後,如上圖,因為是雲端儲存體,所以如果我們要取得Blob,可想而知,就一定是使用url來當作識別的位置啦!如下。

http://<account>.blob.core.windows.net/<container>/<blobname>

來寫程式吧!

接下來,我們就來撰寫程式看看吧,我相信大家應該也沒有那麼多的錢可以租用Windows Azure服務,老實說,小弟我也沒那麼多錢,所以我的範例都是在本機上面做測試。

首先,要寫Windows Azure,必須先裝SDK,如果還沒裝的可以參考這篇。接下來就在Visual Studio裡面選擇Windows Azure專案。

image

到這邊,就可以選擇Web Role,但小弟我不喜歡在這邊選擇,會利用後續再增加的做法,有興趣的可以參考這篇,如果只想要簡單Demo,也可以直接在這邊選擇ASP.NET Web Role ( 中文翻角色 )。

image

接下來,我們先來看看最後寫完的範例程式執行起來的樣子吧;基本上就是長這樣,這隻程式的主要功能是可以上傳照片,並且可以刪除照片,如果有照片的話,可以從DropDownList可以看到檔名,並且於無資料那邊那個區塊,顯示所有雲端上的照片,這個範例也是Ruddy老師所說的,最經典且簡單的Blob範例,所以小弟我也造樣畫葫蘆,寫了此程式,並用此程式和大家解說。

image

首先我們先來看看HTML這邊的程式碼,基本就是很基本的ASP.NET。

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="BlobTest.aspx.cs" Inherits="WebRole1.BlobTest" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        
        <asp:FileUpload ID="uploadImage" runat=server />
        <asp:Button ID="uploadBtn" runat="server" Text="上傳" onclick="uploadBtn_Click" />

        <asp:DropDownList ID="imageList" runat="server" AutoPostBack="true"></asp:DropDownList>
        <asp:Button ID="delBtn" runat="server" Text="刪除" onclick="delBtn_Click" />

        <asp:ListView ID="imageView" runat="server">
            <LayoutTemplate>
                <asp:PlaceHolder ID="itemPlaceHolder" runat="server" />
            </LayoutTemplate>
            <EmptyDataTemplate>
                <h2>無資料</h2>
            </EmptyDataTemplate>
            <ItemTemplate>
                <!--
                Eval:Eval是用於單向資料繫結,資料是唯讀的顯示。
                Bind:Bind則是雙向的資料繫當,不但能讀取資料,
                更具有Insert、Update、Delete功能,所以若您需要編輯更新、新增與刪除功能使用本方法。
                語法如下。
                -->
                <img src="<%# Eval("Uri") %>" alt="<%# Eval("Uri") %>" style="float:left" />
            </ItemTemplate>
        </asp:ListView>
    </div>
    </form>
</body>
</html>

為了大家方便,我也做了一張對應圖給大家看。

圖片 1

接下來,我們開始撰寫這個BlobTest.aspx.cs吧( 此ASP.NET的程式名稱為BlobTest.aspx ),首先,既然要存取雲端的Blob,我們當然要先準備一下,存取的方法;首先,我們定義一個GetContainer的方法,來取得CloudBlobContainer,這樣後續就可以利用此方法來取得Container底下的Blob了;就如前面說的,任何的Container都是在Account之下,所以我們第一步,就是先取得Account,因為我們是在本機上模擬與測試,所以我們可以使用CloudStorageAccount這個靜態類別的DevelopmentStorageAccount這個靜態方法來取得開發用的Account;有了Account,就可以利用Account來取得Client來進行後續的處理,最後,我們就可以利用Client來取得Container ( 這個Container的名稱就是myContainer );到這邊還滿容易理解的,但是有人可能會有疑問,明明沒看到建立mycontainer這個Container的程式碼阿?沒錯,如果第一次執行的時候,到這邊的確還沒有在資料庫裡建立起mycontainer這個Container,我們只是利用client的GetContainerReference(“mycontainer”)這個方法來取得CloudBlobContainer這個物件,並且設定CloudBlobContainer裡面的Name屬性為mycontainer,後續我們才會於資料庫建立Container。

//取得Container
private CloudBlobContainer GetContainer()
{
    //取得Developer用的Storage Account。
    CloudStorageAccount account = CloudStorageAccount.DevelopmentStorageAccount;
    //取得Storage的Client
    CloudBlobClient client = account.CreateCloudBlobClient();
    //取得Container關聯。
    return client.GetContainerReference("mycontainer");
}

有了取得Container這個方法,接下來,我們就來實際撰寫建立容器的方法,第一行滿簡單的,就是取回CloudBlobContainer這個容器,而第二行,我們就利用取得回來的CloudBlobContainer物件來建立一個Container,CreateIfNotExist(),這個方法很好玩,如果目前資料庫裡面已經有了名為mycontainer的這個容器,他就不會再建立了,如果沒有,就會在資料庫裏面建立一個。

//建立容器
private void CreateContainerExists()
{
    CloudBlobContainer container = this.GetContainer();
    //假如沒有這個Container,就建立一個。
    container.CreateIfNotExist();
}

我們可以看到,如果程式執行到這邊,就會建立了一個mycontainer這個container。

( 這是一個很好用的Azure Storage 瀏覽工具 )

image

完成了存取後,我們來處理一下刷新Image這個方法,這個方法的用處是,當有上傳,或是刪除動作的時候,可以呼叫這個方法來重新刷新頁面上的控制項,詳細可以看程式碼的註解。

//刷新Image
private void RefreshImage()
{
    //ListBlobs可以取回此Container所有的Blobs,而後面可以傳入BlobRequestOptions這個物件,
    //這個物件可以設定,傳回來的條件。
    //這邊條件設定的第一個是列出所有的Blob(UseFlatBlobListing = true)
    //並且抓取所有的Blob對象 ( BlobListingDetails = BlobListingDetails.All )
    imageView.DataSource = this.GetContainer().ListBlobs(new BlobRequestOptions() { UseFlatBlobListing = true, BlobListingDetails = BlobListingDetails.All });
    imageView.DataBind();
    //每次刷新時,也清除DropDownList內的資訊。
    imageList.Items.Clear();
    var blobList = this.GetContainer().ListBlobs();
    foreach (var item in blobList)
    {
        //每個Blob都有一個Uri屬性,這裡是取得Blob的檔案名稱。
        imageList.Items.Add(item.Uri.Segments[3].ToString());
    }

    //dropDownListItem其實是紀錄目前dropDownList選了哪一個item。
    //這個變數是此類別的屬性,最後整體程式碼的時候會看到。
    imageList.SelectedIndex = dropDownListItem;

}

接下來,我們來撰寫存Image這個方法,這個方法會帶四個參數進來,第一個會傳入id,後續我們會利用Guid.NewGuid.ToString()來產生id,而第二個會傳入檔案名稱,第三個會傳入檔案的型態( image/jpeg ),第四個則是圖片binary的資料;程式碼的第一行,會使用GetBlobReference來取得Blob物件,這個方法和之前的GetContainerReference一樣,並不會馬上寫到資料庫,指示先將Blob物件準備好,後面的部分,就只是很簡單的設定檔案型態、定義MetaData,最後使用UploadByteArrary將資料寫到資料庫。

//存Image
private void SaveImage(string id, string fileName, string contentType, byte[] data)
{
    //利用GetBlobReference來取得Blob,第一個參數會帶進檔案名稱,
    //而此檔案名稱會化成Uri的一部分。
    CloudBlob blob = this.GetContainer().GetBlobReference(fileName);
    //檔案型態
    blob.Properties.ContentType = contentType;

    //定義MetaData
    NameValueCollection metadata = new NameValueCollection();
    metadata["Id"] = id;
    metadata["Filename"] = fileName;

    blob.Metadata.Add(metadata);
    blob.UploadByteArray(data);
}

最後我們要處理的就是刪除,刪除的原理很簡單,利用DropDownList所選取到的檔名去和娶回來的BlobList來比對,如果相同,就將此Blob移除。

//刪除
protected void delBtn_Click(object sender, EventArgs e)
{
    if (this.GetContainer() == null) return;

    var blobList = this.GetContainer().ListBlobs();
    foreach (var item in blobList)
    {
        string deleteImage = item.Uri.Segments[3].ToString();
        //判斷DropDownList所選擇的item名稱和Blob名稱是否相同,
        //相同就刪除。
        if (deleteImage == imageList.SelectedValue)
        {
            //利用GetBlobReference來取得Blob
            CloudBlob blob = this.GetContainer().GetBlobReference(item.Uri.ToString());
            blob.DeleteIfExists();

            //刪除完後刷新一下。
            RefreshImage();
            return;
        }
    }
}

最後附上完整的程式碼。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

using System.Net;
using System.Collections.Specialized;
using System.Configuration;

using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.StorageClient;
using Microsoft.WindowsAzure.ServiceRuntime;

namespace WebRole1
{
    public partial class BlobTest : System.Web.UI.Page
    {
        int dropDownListItem = 0;
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                //First
                this.CreateContainerExists();
                dropDownListItem = imageList.SelectedIndex;
                this.RefreshImage();
            }
            else
            {
                dropDownListItem = imageList.SelectedIndex;
                this.RefreshImage();
            }
        }

        //上傳
        protected void uploadBtn_Click(object sender, EventArgs e)
        {
            if (uploadImage.HasFile)
            {
                //SaveImage(string id, string fileName, string contentType, byte[] data)
                this.SaveImage(Guid.NewGuid().ToString(), uploadImage.FileName, uploadImage.PostedFile.ContentType, uploadImage.FileBytes);
                RefreshImage();
            }
        }

        //取得Container
        private CloudBlobContainer GetContainer()
        {
            //取得Developer用的Storage Account。
            CloudStorageAccount account = CloudStorageAccount.DevelopmentStorageAccount;
            //取得各種Storage的Client
            CloudBlobClient client = account.CreateCloudBlobClient();
            //取得Container。
            return client.GetContainerReference("mycontainer");
        }

        //確保容器存在
        private void CreateContainerExists()
        {
            CloudBlobContainer container = this.GetContainer();
            //假如沒有這個Container,就建立一個。
            container.CreateIfNotExist();
        }


        //刷新Image
        private void RefreshImage()
        {
            //ListBlobs可以取回此Container所有的Blobs,而後面可以傳入BlobRequestOptions這個物件,
            //這個物件可以設定,傳回來的條件。
            //這邊條件設定的第一個是列出所有的Blob(UseFlatBlobListing = true)
            //並且抓取所有的Blob對象 ( BlobListingDetails = BlobListingDetails.All )
            imageView.DataSource = this.GetContainer().ListBlobs(new BlobRequestOptions() { UseFlatBlobListing = true, BlobListingDetails = BlobListingDetails.All });
            imageView.DataBind();
            //每次刷新時,也清除DropDownList內的資訊。
            imageList.Items.Clear();
            var blobList = this.GetContainer().ListBlobs();
            foreach (var item in blobList)
            {
                //每個Blob都有一個Uri屬性,這裡是取得Blob的檔案名稱。
                imageList.Items.Add(item.Uri.Segments[3].ToString());
            }

            //dropDownListItem其實是紀錄目前dropDownList選了哪一個item。
            //這個變數是此類別的屬性,最後整體程式碼的時候會看到。
            imageList.SelectedIndex = dropDownListItem;

        }

        //存取Image
        private void SaveImage(string id, string fileName, string contentType, byte[] data)
        {
            //利用GetBlobReference來取得Blob,第一個參數會帶進檔案名稱,
            //而此檔案名稱會化成Uri的一部分。
            CloudBlob blob = this.GetContainer().GetBlobReference(fileName);
            //檔案型態
            blob.Properties.ContentType = contentType;

            //定義MetaData
            NameValueCollection metadata = new NameValueCollection();
            metadata["Id"] = id;
            metadata["Filename"] = fileName;

            blob.Metadata.Add(metadata);
            blob.UploadByteArray(data);
        }

        //刪除
        protected void delBtn_Click(object sender, EventArgs e)
        {
            if (this.GetContainer() == null) return;

            var blobList = this.GetContainer().ListBlobs();
            foreach (var item in blobList)
            {
                string deleteImage = item.Uri.Segments[3].ToString();
                //判斷DropDownList所選擇的item名稱和Blob名稱是否相同,
                //相同就刪除。
                if (deleteImage == imageList.SelectedValue)
                {
                    //利用GetBlobReference來取得Blob
                    CloudBlob blob = this.GetContainer().GetBlobReference(item.Uri.ToString());
                    blob.DeleteIfExists();

                    //刪除完後刷新一下。
                    RefreshImage();
                    return;
                }
            }
        }
    }
}

其實,這就是Blob的範例應用,雖然看起來程式碼很長,但其實還滿簡單的。

參考資料

Windows Azure 新增空白Web Role的作法

每次新增Windows Azure專案的時候,可以在下圖選擇Web Role或是Work Role等等之類的,但是我其實不是很喜歡在這邊選擇。因為這裡加上去的專案,都會帶太多範例進來,例如選擇了ASP.NET Web角色 ( 也就是Web Role )的話,預設會將login、首頁等等的範例進來,每次都刪刪減減,所以這邊提供一個增加空白Web Role的作法。

這裡就不選擇了,讓右邊視窗框空白吧。

image_thumb[8]

如果選擇了ASP.NET Web Role,會帶進來太多的範例,例如App_Data、Account的程式、About.aspx、還有長長串的Web.Config。

image_thumb[9]

所以我會先不選,最後再自己加專案進來,下圖是完全沒選的狀況,空空是也!

image_thumb[10]

然後可以再新增專案。

image_thumb[13]

然後就可以選擇空白的ASP.NET Web 專案。

image

然後在原本的Windows Azure專案,再去選擇剛剛增加的專案。

image

然後按下確定就可以了。

image

這樣,就會將Web Role弄進來。

image

但還要注意一些小地方,Web Role是我們自己用加進來的,所以會有些東西沒有參考到,這部分要請大家以下這三個Include進來。

  • Microsoft.WindowsAzure.Diagnostics
  • Mictosoft.WindowsAzure.ServiceRuntime
  • Mictosoft.WindowsAzure.StorageClient

image

這樣就完成了!!

2011年10月30日

點課幫 Developer Party 11-1030活動展開!

活動已經結束,非常感謝大家參加。

11/14更新!

很抱歉因作業繁忙,所以至今才將投影片與資料傳上來,以下是投影片與參考資料。

投影片

相關資料

10/30更新!

非常感謝大家今天熱烈的參與!,關於大家的意見與分數 ( 大家果然都很有佛心~分數給的都好高XDD ),我們也會銘記在心,為了下次的活動,辦得更好,辦的更努力。

今天相關的投影片,我們會再用Mail通知各位,並且在這個頁面公布,請大家稍待幾天>"<…

目前官網也在努力建立中,如果有進一步消息,也會用Mail逐一通知,在此,也非常感謝各位。

以下是照片,礙於人像問題,所以我們只放上投影布幕,其餘的照片,我們會再用Mail告知。

這是第一場,第一次開發iPhone就上手,老實說我也覺得很可惜,沒辦法帶給大家比較多的Demo,未來如果還有類似課程,我會針對比較細節的部分,再去做講說與Demo,此外,這場PhoneGap造成熱烈討論XD,這倒是我沒想到的..未來請Jim開一場PhoneGap好了=w=。

SONY DSC

這是第二場由Cary講解的"如何找出程式與資料庫的效能問題!!",專家出手,果然不同凡響,教了很多監控的方法,未來也會再請Cary多開這種課程吧!

SONY DSC

這是第三場,Microsoft Windows Azure架構探討,果然變成架構探討,後面的Demo完全沒甚麼辦法講…( 前面拖太長… ),而且Demo的時候,竟然忘記開SQL Server Express!!,超瞎的XDD,但還是感謝大家的支持與鼓勵,未來Demo會放到Blog上,讓大家看,未來我也會再針對課程的控制做檢討。

SONY DSC

另外,這次因為場地比較少,但報名人數踴躍,所以場地比較擁擠,這點也請大家多多見諒,我們也會在尋找看看有沒有更好的場地;其次,也感受到大家連續上三堂課,我們也很力飆課程,所以感受到大家到最後都比較沒精神,這點,我們也會檢討是否未來要調整成兩堂,或是課程的內容不要那麼的廣義,總之,無論大家是好的評價,或是壞的評價,對我們都是一種支持,我們也會努力持續下去,讓台中的技術能更加的發光發熱!!

--我是舊資訊分隔線--

這次是第二次的聚會活動了,上次的場地因為已經被預訂走,所以這次換到阿Q茶舍,也讓大家換換口味=w=,這次的主題為手機開發三部曲的第二部分,iPhone APP開發,是由Larry所帶來,他目前是某知名雲端學習的iPhone APP設計師喔!

第二部分是由小弟我所帶來的主題,可惜依舊申請不到免費的Windows Azure帳號,不能當場上傳到雲,但還是會用模擬的方式來Demo一些程式。( 不會又是Hello World了啦! )

第三部分是由Cary講程式與資料庫的效能問題,特別請到某M公司的SQL Server內部技術人員來講解程式與資料庫的效能喔!!,絕對令人期待!!

點課幫 Developer Party 1030 - 有吃有玩又有喝

( 一人最低消費200元~250元,看人數而定)

時間 : 10/30 12:30進場 ,13:00 ~ 17:00
地點:阿Q茶舍 台中市大敦路950號
課程大綱:
Larry - iPhone APP第一次開發就上手
Sky - Microsoft Windows Azure架構探討
Cary - 如何找出程式與資料庫的效能問題!!


檢視較大的地圖

2011年10月24日

Visual Studio 2010 SP1 重新安裝 Silverlight 4 Tools

最近因為要開始進行Silverlight的專案,所以準備塵封已久的Silverlight,結果一新增專案,就給我跳出這個畫面。內容敘述是這樣

"以Silverlight 4為目標必須有Microsoft Visual Studio的更新。"

image

想說,別鬧了,我都已經裝了SP1,SP1也內含了Silverlight 4,還要叫我更新啥= =,但礙於找不到其他解決方案,所以我就下載了Silverlight 4 Tool來重新安裝。

結果很開心的又出現下面這個畫面。

必須先安裝符合 Silverlight Tools 4 的語言版本的 Visual Studio 2010 或 Visual Web Developer Express 2010 或 Visual Phone Developer Express 2010,才能繼續安裝 Silverlight Tools 4。Silverlight Tools 有其他語言版本: http://go.microsoft.com/fwlink/?LinkId=177432">http://go.microsoft.com/fwlink/?LinkId=177432

image

好樣的!,後來查了Log檔,發現此支援版本為

Package Version = 10.0.30319 1033

但我們目前的Visual Studio 2010 SP1版本為10.0.40219,所以猜測是這個問題。

image

既然知道問題在哪邊,那就好辦了,首先,我們先把解壓縮軟體把Silverlight4_Tools.exe解開,然後開啟文字編輯工具編輯ParameterInfo.xml這個檔案。

image

然後將10.0.30319改成10.0.40219,如下圖。( 這個檔案裏面有許多的10.0.30319,下面的圖只是其中一個位置 )

image

改完後就可以順利安裝。

image

至於為何會發生這種問題,我倒是沒有頭緒,只記得最後一次裝了和Silverlight有關的軟體是Expression Studio 4,但這些也都是推測,畢竟,小弟我的電腦常常裝一堆新東西XDD,所以搞不好是別的東西影響到的,但不管怎樣,這樣處理完後,就可以順利安裝,安裝完後Silverlight 4也恢復正常了。

題外話 : 後來發現,Google已經有許多類似的教學了= =….,那我幹啥花那麼多時間除錯啊…

2011年10月13日

UI的設計模式MVP模式–Supervising Controller

最近雜事一堆,所以一直沒有空把MVP模式的Supervising Controller給補完,今天終於下定決心,把MVP的另外一個模式Supervising Controller給補完吧!

斯斯有兩種,MVP也有兩種

MVP設計模式有分兩種,最先提出來的是Martin Fowler,最後於2006年將MVP分成Supervising Controller和Passive View;基本上這兩個的概念是相同的,但最大的差別只在於那個"P" ( 也就是Presenter ),Presenter能控制的東西程度不同罷了,Supervising Controller就沒辦法完全掌控Model。而這次我們要看的是其中一個MVP模式Supervising Controller

Supervising Controller

如果已經有讀過Presenter View的人,可以看一下架構圖後,跳到最後一段。

所謂沒圖沒真相,沒程式碼就不叫寫程式,所以我們第一件事情,先給個Supervising Controller的圖吧。

image

沒錯,這是MVP Supervising Controller的圖,不是MVC的,我相信如果有看過MVC的人,大概也找不太出兩者間的差異吧。那至於這個架構圖和MVC有甚麼不一樣!?,好問題!,但我們留到後面再看。

正式開始

假設今天我們要用ASP.NET寫一個查詢程式,非常非常簡單的查詢,只要輸入,名子,就會尋找到指定的資料,並且顯示於下方,畫面大致上是這樣。

image_thumb10

啥!?很醜!!,而且又和前面的Presenter View一樣!?那是因為後面要比較的關係嘛,就不要太苛求了,總之,就是在TextBox輸入名子,按下Button後,會在下面兩個Label顯示姓名和地址…

image_thumb9

程式碼大概長這樣。

我們會先有一個User的類別,內容很簡單,就兩個屬性,名子和地址。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace WebAppMvpDemo
{
    public class User
    {
        public string name { get; set; }
        public string address { get; set; }
    }
}

接下來,我們實作一下存取User的類別,簡單的說,要存取User都是透過這個類別來和底層的資料傳遞;當然,既然是範例,就沒有和真實的資料庫做一個交流,也只是很單純的定義一個方法GetByName,且當輸入Sky的時候,就會傳回一個User物件回來;如果是正式的環境,我們這裡可能使用SQL存取,也可能使用ORM技術存取。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace WebAppMvpDemo
{
    public class UserRepository
    {
        public User GetByName(string userName)
        {
            //作假資料,如果搜尋到Sky,就new一個新的user回傳。
            if (userName == "Sky")
            {
                User user = new User();

                user.name = "Sky";
                user.address = "台中";
                return user;
            }
            return null;
        }
    }
}

接下來,是視覺的頁面,這頁沒甚麼,就很一般,把一些東西拖拖、拉拉,就好了,頁面有點長就是了。

<%@ Page Title="首頁" Language="C#" AutoEventWireup="true"
    CodeBehind="Default.aspx.cs" Inherits="WebAppMvpDemo._Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
</head>
<body>
    <form id="Form1" runat="server">
    <div class="page">
        <div class="header">
            <div class="title">
                <h1>
                    MVP架構測試
                </h1>
            </div>
        </div>
        <div class="main">
            <div>
                <h2>歡迎使用 ASP.NET!</h2>
                <p>此為MVP架構測試。</p>
            </div>
            <div>
                <asp:TextBox ID="SearchUserTextBox" runat="server"></asp:TextBox>
                <asp:Button ID="Button1" runat="server" Text="Button" onclick="Button1_Click" />
           </div>
           <div>
                <asp:Label ID="UserNameLabel" runat="server" Text="Label"></asp:Label>
                <asp:Label ID="UserAddressLabel" runat="server" Text="Label"></asp:Label>
           </div>
        </div>
    </div>
    </form>
</body>
</html>

接下來是主要的程式碼,基本上就是在處理Button的Click事件,我們會先把剛剛寫好,專門用來存取User類別的UserRepository類別建立起來,來方便我們存取User,然後使用UserRepository的GetByName方法來尋找使用者;當然,事前會稍微做一些簡單的判斷,看看是不是空值之類的;

( 關於資料存取,這種做法是"比較"好一點的作法,通常我們會設計一個介面,來降低與UserRepository的耦合,這才是Repository模式的做法,不過這邊為了敘述方便,就不透過介面了,看不懂的人也沒關係,可以先跳過;此外,本來還想直接寫SQL在Click事件裡面,但想到這樣還要準備資料庫,發現更累XDD,所以最後還是用這種方法來Demo,而SQL散落於世界各地的寫法,其實是最不好的寫法,不過我以前公司幾乎都是這樣寫就是了XDD )

從UserRepository會吐回來User物件,然後我們把值顯示於ASP.NET 控制項,就完成了這簡單的Demo。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace WebAppMvpDemo
{
    public partial class _Default : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {

        }

        protected void Button1_Click(object sender, EventArgs e)
        {
            UserRepository userRepository = new UserRepository();
            if (string.IsNullOrEmpty(SearchUserTextBox.Text))
            {
                return;
            }

            User user = userRepository.GetByName(SearchUserTextBox.Text);
            if (user == null)
            {
                return;
            }

            UserNameLabel.Text = user.name;
            UserAddressLabel.Text = user.address;
        }
    }
}

沒錯,這幾乎就是一般的ASP.NET 開發的寫法,雖然比asp好太多了,但還是耦合得太緊,沒辦法測試,如果要測試的話,還是必須開啟網頁,填填看TextBox,然後按下Button,其次就是維護也不容易,所以接下來,我們來把這個範例改成MVP的Supervising Conroller試試看。

首先,我們先處理Model的部分,但其實,Model我們早就已經寫好了,也就是User這個類別,而這個類別改成MVP的Supervising Controller架構,其實也不需要做變動,但是Repository模式的這個部分還沒做好,我們先定義一個介面IUserRepository。

( Repository模式和MVP Supervising Controller模式,兩者之間並不是必須,也就是說,今天我實作MVP Supervising Controller,是可以不用做Repository模式的,但Repository模式可以讓整個資料層分離的更好,所以就一起講吧=w= )

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace WebAppMvpDemo
{
    public interface IUserRepository
    {
        User GetByName(string userName);
    }
}

然後裡面再實作這個介面;不過剛剛我們已經先做好了,也就是UserRepository,其實這邊的程式碼和之前的UserRepository沒甚麼差別,唯一的差異只有UserRepository實作IUserRepository,也就是這行。

public class UserRepository : IUserRepository

以下是UserRepository完整的程式碼。

( 為什麼要做一個介面?其實原因很簡單,就是為了方便我們測試時抽換,只要有實作此介面的實體,就可以輕易地去抽換掉,後面會繼續講解。 )

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace WebAppMvpDemo
{
    public class UserRepository : IUserRepository
    {
        public User GetByName(string userName)
        {
            //作假資料,如果搜尋到Sky,就new一個新的user回傳。
            if (userName == "Sky")
            {
                User user = new User();

                user.name = "Sky";
                user.address = "台中";
                return user;
            }
            return null;
        }
    }
}

好的,完成了以後,接下來我們來實作View的部分,那View在哪裡?,以我這邊的範例,頁面是SupervisingControllerView.aspx,所以,View就是SupervisingControllerView.aspx和SupervisingControllerView.cs檔。

而在準備View之前,也必須先準備好Interface,"ISupervisingControllerDetialView”,然後裡面定義了SearchUser、user兩個必須實作的方法,其實這兩個方法,就是未來要給Presenter傳遞給View要顯示用的。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace WebAppMvpDemo
{
    public interface ISupervisingControllerDetialView
    {
        string SearchUser { get; }
        User user { set; }
    }
}

ok~接下來我們要處理SupervisingControllerView.cs了,雖然程式很簡單,但還是很長,而且有很多地方需要注意的,首先,我們會讓此類別( SupervisingControllerView )去實作此介面,目的也是一樣,為了降低耦合與測試。而我們也會在這個類別裡面去實作Presenter,目前我們還沒寫到Presenter,但這邊會用到Presenter的原因其實很簡單,因為是由Presenter來控制,如最下面的Button Click所看到,我們沒有將程式寫在SupervisingControllerView裡面了,而是去呼叫Presenter的OnUserSearched方法;接下來往回看一點,有兩個存取控制項的方法,這些方法會利用Model來回傳或是設定控制項 ( 這個範例我們使用User這個Model去塞到控制項裡面去做顯示 );最後,最關鍵的是new Presenter(this)這段,這段也就是說把SupervisingControllerView塞到Presenter裡面去,讓Presenter能參照到SupervisingControllerView,這代表甚麼呢?也就是說,Presenter可以控制到SupervisingControllerView,所以Presenter裡面不會再有SearchUserTextBox.Text這種和控制項有關的程式碼,而通通都是透過SupervisingControllerView所定義的方法來取得控制項的資訊,換言之,Presenter和控制項再也無關,View就是View,Presenter就是Presenter,個兼其職。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace WebAppMvpDemo
{
    //讓此類別實作IDefault2DetialView
    public partial class SupervisingControllerView : System.Web.UI.Page, ISupervisingControllerDetialView
    {
        //目前還沒寫到此類別。也就是MVP的P
        private SupervisingControllerPresenter _presenter;

        protected void Page_Load(object sender, EventArgs e)
        {
            //將自己,也就是SupervisingControllerView注入到P裡面去,目的是為了讓P能控制View。
            _presenter = new SupervisingControllerPresenter(this);
        }

        public User user
        {
            //直接讓View去繫結Model,
            //所以在架構圖上View和Model有一條線的原因就在這裡。
            set { 
                UserAddressLabel.Text = value.address;
                UserNameLabel.Text = value.name; 
            }
        }


        public string SearchUser
        {
            get { return SearchUserTextBox.Text; }
        }

        protected void Button1_Click(object sender, EventArgs e)
        {
            //當Click事件發生的時候,不由View處理,而是轉交給Presenter處理。
            _presenter.OnUserSearched();
        }

    }
}

然後我們看一下Presenter,其實Presenter還比較簡單一點,如果你已經看到這邊,恭喜你,快解脫了XDD;Presenter,會定義兩個介面,而利用建構子的方式傳進有實作這兩個介面的物件,換言之,當我們要測試的時候,就可以傳入有實作這兩個介面的物件進來 ( 通常我們使用Mock來做模擬物件 ),而這也是為什麼前面要一直定義介面的關係,就是為了降低耦合,讓測試能更好進行,至於OnUserSearched方法,其實和之前的程式沒有多大改變,就只是進行了資料撈取的動作,只是不會直接寫到控制項裡面去了,而是會透過實作ISupervisingControllerDetialView的物件來進行存取的動作,好處就如之前說的,隔離了控制項的部分,讓耦合降更低。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace WebAppMvpDemo
{
    public class SupervisingControllerPresenter
    {
        //定義這兩個型別,這也是關鍵,也就是說,我們可以自行去設計並抽換。
        //也因此和資料存取還有View的部分就沒那麼緊密的耦合。
        private readonly IUserRepository _userRepository;
        private readonly ISupervisingControllerDetialView _view;

        //這裡的建構子使用一種叫做建構子鍊的方式,
        //但關注的是,我們利用建構子來傳進有實作的介面的物件,
        //換言之,我們可以自行去設計,只要有實作介面即可。
        //( 但是比較建議使用Moq方式,可以再參考我的Blog.... )
        public SupervisingControllerPresenter(ISupervisingControllerDetialView view)
            : this(view, new UserRepository())
        {
        }

        public SupervisingControllerPresenter(ISupervisingControllerDetialView view, IUserRepository userService)
        {
            _view = view;
            _userRepository = userService;
        }

        //這裡定義一個方法,就是當按下Button時,View會進行此方法的呼叫。
        public void OnUserSearched()
        {
            if (string.IsNullOrEmpty(_view.SearchUser))
            {
                return;
            }

            User user = _userRepository.GetByName(_view.SearchUser);
            if (user == null)
            {
                return;
            }

            //和Presenter View最大的差異,傳遞給View的不再是User的屬性,
            //而是整個Model
            _view.user = user;

        }
    }
}

結論

其實到這邊,使用Supervising Controller架構的程式已經完成了。

我們可以看到,整體是透過Presenter來進行整個的控制,當View有事件發生時,會轉交給Presenter來進行處理,而Presenter也會利用View給的介面來進行資料的存取;另一方面,透過各種資料存取方式來取得的Model,也會利用Presenter來傳遞給View。

最後照老規局,我們把圖重新貼回來看看,如下圖,View和Presenter的關係是雙向的,但是Presenter透過ISupervisingControllerDatialView這個介面來降低與View的耦合,也就是說,Presenter可以不用太在乎View,可以更方便的去進行測試;另一方面,Model最為獨立,所以可以專心的去開發Model,而View的部分,就專心的去呈現頁面的顯示;所以結論就可以很輕易地去拆開進行測試!

image

Presenter View的差異

如果之前有讀過Presenter View,然後還把這篇讀完的人,真的辛苦了,但是兩者間的差異其實非常小,而且其實也只差異於View和Model之間的那條線。

簡單的說Presenter View的Presenter是完全掌控Model,View提供的是所有控制項的接口,但是Supervising Controller裡面,Presenter則沒有完全的去控制,而是將Model傳遞到View裡面,換言之Supervising Controller裡面的View,提供不在是一個一個的控制項接口,而是可以直接將Model傳遞進去的方法,如下,是Supervising Conroller的View程式碼。

public User user
{
    //直接讓View去繫結Model,
    //所以在架構圖上View和Model有一條線的原因就在這裡。
    set { 
        UserAddressLabel.Text = value.address;
        UserNameLabel.Text = value.name; 
    }
}

而這裡的是Presenter View的程式碼。

public string UserName
{
    set { UserNameLabel.Text = value; }
}

public string UserAddress
{
    set { UserAddressLabel.Text = value; }
}

而在Presenter裡面的差異,Supervising Controller程式碼

//這裡定義一個方法,就是當按下Button時,View會進行此方法的呼叫。
public void OnUserSearched()
{
    if (string.IsNullOrEmpty(_view.SearchUser))
    {
        return;
    }

    User user = _userRepository.GetByName(_view.SearchUser);
    if (user == null)
    {
        return;
    }

    //和Presenter View最大的差異,傳遞給View的不再是User的屬性,
    //而是整個Model
    _view.user = user;

}

另外,這是Presenter View的程式碼,差別就在於Presenter需要一個一個去設定。

//這裡定義一個方法,就是當按下Button時,View會進行此方法的呼叫。
public void OnUserSearched()
{
    if (string.IsNullOrEmpty(_view.SearchUser))
    {
        return;
    }

    User user = _userRepository.GetByName(_view.SearchUser);
    if (user == null)
    {
        return;
    }

    _view.UserName = user.name;
    _view.UserAddress = user.address;

}

換言之,Supervising Controller的程式碼比較簡潔,畢竟,如果今天有好幾個頁面需要塞入幾十個控制項,Presenter View就會顯得複雜,但同樣的,如果今天的頁面邏輯是很複雜的( 例如又要搜尋,又要顯示下一筆,又要顯示客戶的訂單明細等等等 ),那Supervising Controller的處理就變得不好去測試了,所以也有人說Supervising Controller比較適合ASP.NET,而Presenter View比較適合Win Form架構。

MVC 和 MVP

如果是拿Presenter View和MVC比,大概也沒什麼好比的,因為兩者差異性還滿大的,但是如果Supervising Controller和MVC相比,其實MVC是比Supervising Controller更早出來,而Supervising Controller的演進也是由Presenter View而來,目的是為了處理當時的ASP.NET架構( 沒想到最後又回到ASP.NET MVC的MVC架構 ),如果要講,我們可以說Supervising Controller是有狀態的,而MVC是適用於無狀態的;MVC架構下的Controller和View的繫結沒那麼深,以MVC來說,當請求到Controller,並決定哪個Action產出View後,就和Controller沒關係了,直到View送出Post時,會再回到Controller裡面,但是Post後的Controller和第一次的Controller,雖然可能是同一個類別,但兩者卻沒有任何關係( 也就是說沒有狀態,不需要維持與View之間的關係 ),但是如果是Supervising Controller來看,View送出Post後,回到Presenter,那一定是原來的Presenter( 也就是有狀態的關係,會和View進行維持 ),所以ASP.NET想要真的實作MVC是有難度的( 因為一堆ViewState和ControlState,微軟就是要讓ASP.NET有狀態… ),而這也是最大的差異。

最後

終於把三大M家族講得差不多了 ( 好吧,我承認還有近期WPF、Silverlight使用的MVVM..QQ ),雖然這些東西的差異感覺都很小,但本質上卻有很大的差異,如何運用,該用在怎樣的地方,就變成一個學問,也希望大家耐心看完後,能真正的了解M家族的不同。

2011年10月12日

Windows Server DNS 414 警告 (錯誤)

為了防止我老人痴呆=w=,所以把這個事件紀錄下來,這是我剛安裝完Windows Server的DNS服務後發生的警告,不過感覺上不理他也不會有影響,但由於該死的完美心作祟,就把他給解決了,也在這邊順便分享一下。

下面是從伺服器管理員所看到的問題,主要是414警告。

image

內容點開是這樣。

image

以下是把他拉出來的文字描述


DNS 伺服器電腦目前沒有 DNS 網域名稱。它的 DNS 名稱是沒有網域的單一標籤主機名稱 (例如: "host" 而不是 "host.microsoft.com")。
 
您可能忘了為伺服器電腦設定主要的 DNS 網域。
 
因為 DNS 伺服器只有單一標籤名稱,所有建立區域將只會使用這個單一標籤的主機名稱來建立預設的記錄 (SOA 和 NS)。當用戶端或其他的 DNS 伺服器使用這些記錄依名稱尋找伺服器時,將會造成轉介不正確及失敗。
 
如果您要修正這個問題,請:
  1) 按一下 [開始],然後按一下 [控制台]。
  2) 開啟 [系統及維護],然後開啟 [系統]。
  3) 按一下 [變更設定],然後按一下 [變更]。  4) 按一下 [網域] 或 [工作群組],然後輸入您希望電腦加入的網域或工作群組名稱; 網域或工作群組名稱將會做為您的 DNS 網域名稱。
  5) 當提示時,重新啟動電腦。
 
在電腦重新啟動之後,DNS 伺服器會修改預設記錄,以新的伺服器 DNS 名稱取代舊的單一標籤名稱。不過,您應該檢查一次,確認目前區域的 SOA 和 NS 記錄使用正確的伺服器名稱。

老實說解決步驟,光是到"系統及維護",我就找不到了XDD,不過大概還是猜的到他的意思,以下是處理方法。

首先要給一個電腦名稱,然後再按其他,繼續設定。

image

接下來,在"這部電腦的主要DNS尾碼"加上Domain Name。

image

最後,按下確定,重新開機,就一切搞定!

2011年10月11日

ASP.NET MVC - 可以移除Model的驗證嗎!?

開始前,我就直接講答案了,原則上,是不行於Runtime移除Model設定的驗證,但是有列出幾種做法可以給大家參考看看。

我們都知道ASP.NET MVC的Model驗證是非常強大又好用的,我們只要在Model裡面定義好,後續無論是Client端,或是Server端,都可以自動產生出驗證功能,來讓我們方便的使用。

有些地方需要特定的Model驗證,有些地方不需要!?

這是我使用Model驗證碰到的一個問題,舉例來說,我新增Customer和更新Customer的地方,客戶編號、客戶姓名是必填,所以我的Customer的地方會有這樣的程式碼。

[DisplayName("客戶編號")]
[Required(ErrorMessage = "請輸入客戶編號")]
[Range(1, 10000,ErrorMessage="範圍值為1~99999")]
public virtual object Sn { get; set; }

[DisplayName("中文名稱")]
[Required(ErrorMessage = "請輸入中文名稱")]
[StringLength(32, ErrorMessage = "請勿輸入超過32個字")]
public virtual object CName { get; set; }

這樣是很合理的Model驗證寫法,但是,今天碰到一個問題,如果我的Search裡面也有這些欄位要填寫,但是Sn和CName卻不是必填,這時候該怎麼辦!?

幾種解法 – Server端解法

第一種解法,就是把Required這個屬性欄位拿掉,這樣就不會驗證到未填寫的錯誤,但是換言之,新增與修改的地方也變成可以不用填寫這兩個欄位;這樣當然不行啊!!,所以我們可以在Controller裡面加上ModelState.AddModelError()這個方法。

[HttpPost]
public ActionResult Add(Customer customer)
{
    //利用ModelState.AddModelError來產生錯誤
    if (customer.Sn == null)
        ModelState.AddModelError("Sn", "請輸入客戶編號");
    if (string.IsNullOrEmpty(customer.CName))
        ModelState.AddModelError("CName", "請輸入中文名稱");

    if (!ModelState.IsValid)
    {
        return View(cust);
    }
    else
    {
        //寫入資料庫程式碼...
    }
}

ModelState.AddModelError()會產生一個錯誤訊息,所以我們可以利用這種方式來替代原本拿掉的Required,但是這種解法也有缺點,如上面所示,新增的Controller也要改,更新的Controller也要改,而且這種方法,也只有Server端的驗證,Client是沒有作用的。

幾種解法 – Client端解法

剛剛是Server端的解法,我們可以搭配Client端的驗證,其實在ASP.NET MVC 3的時候就開始配合HTML5,並導入了data-val的屬性,利用這個屬性,就可以輕鬆地去自訂驗證,( 當然還是要配合jQuery啦 ),我們可以看到其實透過ASP.NET MVC的Html.Helper配合Model,就會自動產生以下的程式碼,而每個data-val-*,其實就是對應到Model所設定的驗證,所以我們可以在新增與更新的頁面,不使用Html.EditorFor來產生HTML,而是用手刻的方式去自己寫HTML,如下:

<dt><label for="">客戶編號</label> :</dt>
<dd><input class="text-box single-line" data-val="true" data-val-number="欄位 客戶編號 必須是數字。" 
data-val-range="範圍值為1~99999" data-val-range-max="10000" data-val-range-min="1" data-val-required="請輸入客戶編號"
id="Sn" name="Sn" type="text" /></dd>
<dd><span class="field-validation-valid" data-valmsg-for="CusNo" data-valmsg-replace="true"></span></dd>

<dt><label for="CName">中文姓名</label>:</dt>
<dd><input class="text-box single-line" data-val="true" data-val-length="請勿輸入超過32個字" 
data-val-length-max="32" data-val-required="請輸入中文名稱" id="CName" name="CName" type="text" value="" /></dd>
<dd><span class="field-validation-valid" data-valmsg-for="CName" data-valmsg-replace="true"></span></dd>

這個方法的重點就在於手工加上data-val-required屬性來驗證,搭配Server端,就可以達到Client和Server的驗證,但缺點還是一樣,很多頁面,就要改很多次,也變得比較麻煩。

幾種解法 – ViewModel

其實這個沒什麼技巧,只是針對Search這個頁面特別的去寫一個Model,( 針對某頁面而產生的Model,稱為ViewMode ),然後所有的驗證就可以於ViewModel裡面去撰寫處理,這樣就可以和新增還有修改的地方去做隔離,缺點是要做多建立一個ViewModel,如下,我們建立一個SearchCustomerViewModel。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel;

namespace SkyTestViewModel
{
    public class SearchCustomerViewModel
    {

        [DisplayName("客戶編號")]
        [Range(1, 10000, ErrorMessage = "範圍值為1~99999")]
        public Nullable Sn { get; set; }

        [DisplayName("中文名稱")]
        [StringLength(32, ErrorMessage = "請勿輸入超過32個字")]
        public string CName { get; set; }

    }
}

其實和一般我們常用的Model沒什麼差異,接下來我們看看View的部分。

@model SkyTestViewModel.SearchCustomerViewModel
           
<fieldset>
    <legend>快速搜尋</legend>
    <dl>
        <dt>@Html.LabelFor(model => model.Sn):</dt>
        <dd>@Html.EditorFor(model => model.Sn)</dd>
        <dd>@Html.ValidationMessageFor(model => model.Sn)</dd>

        <dt>@Html.LabelFor(model => model.CName):</dt>
        <dd>@Html.EditorFor(model => model.CName)</dd>
        <dd>@Html.ValidationMessageFor(model => model.CName)</dd>

    </dl>
</fieldset>
<input type="submit" value="查詢" />

這裡也和一般沒兩樣,只不過我們把model的部分換成SearchCustomerViewModel而已,然後是Controller的部分。

[HttpGet]
public ActionResult SearchResult(SearchCustomerViewModel SearchCustomerViewModel)
{
    //取得資料...並且秀出資料
}

後續就使用SearchCustomerViewModel進行處理,當然,這種最大的缺點就是會產生兩個幾乎類似的類別,維護也會變得比較麻煩,但這種還是最推薦的做法,而且反過來看,真的是維護比較麻煩嗎?,假設說Customer裡面有三個Tel欄位,但是SearchCustomerViewMode原則上只會有一個Tel欄位來輸入,所以這兩種類別看起來一樣,但本質上卻是不同的了,所以分開反而是一件好事,另外,如果真的覺得維護很麻煩,也有AutoMapperValueInjecter這兩種方法來進行映射,但繼續介紹下去,就越離越遠了XDD…

結論

如一開始所講的,沒辦法動態時期移除Model的驗證,而根據國外大師們的建議,也認為ViewModel是比較好的一種做法,但有的時候,或許上面的其他解法才是適合你的,畢竟解法並非只有一種,根據不同的狀況,用不同的解法,才是最好的。

ASP.NET MVC - WebGrid配合Ajax動態刪除WebGrid的Row

WebGrid是ASP.NET MVC 3(含)以上版本才有的Html Helper,可以快速地將資料利用正規的HTML Table排列出來,簡單的說,就很像是以前的GridView。

前天我朋友問了我幾個問題,其中一個問題就是WebGrid,內容大致上是敘說,當資料利用WebGrid產生,而且裡面會配合Ajax的Delete功能,當按下刪除的時候,要怎才能動態刪除WebGrid的Row( 也就是Table的tr ),想當然爾,我很爽快的說,就配合jQuery,當刪除成功的時候,移除掉Row,就好啦;然後我朋友說,有那麼簡單,做給我看看吧!,結果,我花了一整晚的時間…。( 所以沒事不要太臭屁… )

平常我是用手工打造Table的!

其實,我平常的時候,還是會常常自己去利用Foreach產生Table,所以這種問題其實對手工打造的我沒甚麼影響,只要在每個迴圈中間,將每個tr的插入特定的id,到時候刪除的時候,再利用此id進行hide或是整個移除的動作,就可以了;但遇到WebGrid,當場我就發現了,感覺沒那麼簡單( 所以花了一整晚的時間尋找解法… ),因為WebGrid雖然可以於td裡面的標籤插入id,但是我卻找不到能在tr插入id的方法,後來有找到webgrid的selected等等的方法,但是感覺又沒有那麼的好做,所以最後我還是決定採用我自己的做法。

感覺不是很漂亮的方法

目前我使用的方法是在WebGrid產生的td標籤裡面的Delete( a標籤 )來插入id,然後再用jQuery的篩選器,來取得a標籤的父層標籤td,然後再取得td標籤的父層標籤( 也就是tr ),然後再隱藏整個tr,當然,我還是覺得不夠優雅XD,如果有更好的方法,未來再補上吧。

以下是程式碼:

@model IEnumerable<Customer>

@*
因為webGrid裡面不能設定tr id,所以將id設在於Delete a標籤內,
再利用jQuery取得父元素。
*@
<script type="text/javascript">
    function RemoveData(sn) {
        var trObject = $("#" + sn).parent().parent();
        trObject.hide("slow");
}
</script>

<div id="gridDiv">
@{var grid = new WebGrid(source: Model, defaultSort: "sn", ajaxUpdateContainerId: "gridDiv", rowsPerPage: 10);}

@*這裡特別注意Delete的地方,有特別設計tdNum作為辨識之用,如果刪除成功的話,就會呼叫RemoveData這個js,並把tdNum這個id傳入,
以利隱藏刪除後的Html*@
@grid.GetHtml(htmlAttributes: new { id = "grid" },
        columns: grid.Columns(
          grid.Column(format: item => Html.ActionLink("Edit", "Edit", new { id = item.sn })),
          grid.Column(format: item => Ajax.ActionLink("Delete", "Del", new { id = item.sn },
                              new AjaxOptions() { HttpMethod = "Delete", Confirm = "是否確定刪除?",
                                                  OnSuccess = string.Format("RemoveData('tdNum{0}')", item.sn ),
                              },
                          new { id = string.Format("tdNum{0}", item.sn) })),
          grid.Column("cname", "中文名稱")
     )
)
</div>

這樣就解決了…

2011年10月6日

進入ASP.NET - ASP.NET的起點

最近開始深入ASP.NET的核心,花了很多時間來了解,也翻了很多書還有找了很多很多網站,同樣的如果不寫這篇,我想大概沒幾天我就忘記了,就像是委派一樣,看過四次,但還是只剩一些殘餘的印象XDD ( 所以下次一定要把委派整理起來- - ),所以我決定還是稱現在比較清楚的時候,趕快整理起來,並且寫到Blog來做個紀錄。

ASP.NET 核心

這系列是ASP.NET核心篇的起點,也就是ASP.NET底層是怎樣運作的,從一個HTTP Request近來的時候,會怎樣處理,Web應用程式又是怎樣吐回到Client的Browser,老實說,看完後,我覺得還滿有趣的 ( 但是看的時候一點也不有趣= = ),而這系列也是比較硬派一點的文章,我也試著用我了解的方式且比較多的例子來詮釋一遍,當然,如果有神人發現有誤,請務必和我說喔!

來寫一個Web Server吧!

當個使用者其實是最開心的,甚麼都可以不用管,出問題了還可罵說,怎麼寫得那麼爛…,但是身為程式開發人員就倒楣了,不但要被罵,還要找出哪裡有問題…,所以我們也必須去了解底層是怎樣運作,所以我們這邊簡單的從Browser開始講起;其實當一個Browser要求一個網頁的時候,其實是一堆文字的來來回回,也就是透過協定來進行Client和Server的傳輸 ( 細節就不說了,如果有機會,我在另外一篇補完 ); 而我們平常使用的IIS或是Apache等等Web Server,提供的網頁服務,就是再處理Browser的請求;當Browser需要一個東西,Web Server就吐回去一個東西,ok,我相信講到這邊,大家應該都沒有問題,那既然我們也知道協定、傳輸等等方式,是不是可以用C#等等語言寫一個Web Server!?,答案當然是可以,但我想應該沒有人會那麼無聊,寫出像IIS或是Apache那樣強大的Web Server…( 或是說,也很難寫出來,還是乖乖地用人家寫的吧 ),雖然無法寫出那麼強大的Web Server,但是簡單的還是可以吧!?,是的,就像下面的程式碼;不過不用急著去把它看完 ( 我想看到這麼長的程式,大概就會想直接按"上一頁"了吧… );它的原理其實很簡單,利用TcpListener這個類別來監聽,聽到請求後就將資料吐回,當然你也可以用Socket這個類別或是HttpListener來實作;總之,我想強調的是,基本上Client和Server端通訊內容,其實也就是一堆文字,也就是利用這些文字來處理要做甚麼事情。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.IO;
using System.Threading;
using System.Web;
using System.Web.Hosting;
using System.Net;

namespace AspNetSimulator
{
    class WebHandle
    {
        private TcpListener _tcpListener;    //監聽用
        private FileStream _fileStream;      //利用串流處理要求。
        private string _virtualDir;
        private string _realDir;

        public WebHandle(string virtualDir, string realDir)
        {
            this._virtualDir = virtualDir;
            this._realDir = realDir;
        }

        //啟動服務
        public void StartService()
        {
            try
            {
                IPEndPoint address = new IPEndPoint(IPAddress.Loopback, 8080);
                _tcpListener = new TcpListener(address);
                //啟動監聽
                _tcpListener.Start();   
                Console.WriteLine("啟動服務...");
                
                WebServiceStart();
            }
            catch (NullReferenceException)
            {
                Console.WriteLine("NullReferenceException throwed!");
            }
        }

        //主要服務
        public void WebServiceStart()
        {
            while (true)
            {
                //建立連線
                TcpClient tcpClient = _tcpListener.AcceptTcpClient();
                Console.WriteLine("連線建立...");
                //取得網路串流
                NetworkStream networkStream = tcpClient.GetStream();
                //設定使用UTF-8
                Encoding utf8 = Encoding.UTF8;
                byte[] request = new byte[4096];
                //讀取請求,並塞到request裡面,此方法會傳回長度。
                int length = networkStream.Read(request, 0, 4096);
                //轉換成人類能看得懂的字串。
                string requestString = utf8.GetString(request, 0, length);

                string output = "";
                //中間略,簡單的說就是將網頁輸出到output...

                //以下只是簡單的將訊息送出。
                string statusLine = "HTTP/1.1 200 OK\r\n";
                byte[] outputStatusLineBs = utf8.GetBytes(statusLine);
                byte[] outputBodyBs = utf8.GetBytes(output);
                string responseHeader = string.Format("Content-Type: text/html;charset=UTF-8\r\nContent-Length:{0}\r\n",outputBodyBs.Length);
                byte[] outputResponseHeaderBs = utf8.GetBytes(responseHeader);
                networkStream.Write(outputStatusLineBs, 0, outputStatusLineBs.Length);
                networkStream.Write(outputResponseHeaderBs, 0, outputResponseHeaderBs.Length);
                networkStream.Write(new byte[]{13,10},0,2);
                networkStream.Write(outputBodyBs, 0, outputBodyBs.Length);
                tcpClient.Close();
            }
        }
    }
}

.NET的Application Domain

在談論ASP.NET之前,先說一個觀念,那就是Application Domain,他是.NET裡面管理的最小單位,舉例來說,每個Web應用程式會有一個Application Domain ( 和 Application Pool不同…),而Web Server也會有一個Application Domain;而Application Domain會有一些特性,例如說,兩個Application Domain不能直接互通、執行時,以Application Domian為邊界、卸載的時候也以Application Domain為單位 ( 但是可以隨時動態載入組件 )等等。

為何會提到Application Domain?

其實是因為Application Domain有一個關鍵性的特點,發現了嗎?就是兩個Application Domain不能直接互通!,簡單的說,當一個Web Server收到Client的請求時,Web Server沒辦法跨Application Domain到Web應用程式的Application Domain來讓Web應用程式處理這個請求 ( 也就是說Web應用程式沒辦法得知這個Client的請求,所以也沒辦法處理,然後吐回處理過的html網頁… )。

MarshalByRefObject是救星!

沒錯,解決辦法就是使用MarshalByRefObject這個類別,繼承了這個類別的物件,可以讓Web Server透過他來將資料封裝起來並傳遞到Web應用程式裡面去,然後再吐回Web Server。

來看看MarshalByRefObject如何寫

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Web.Hosting;
using System.Web;

namespace AspNetSimulator
{
    //繼承MarshalByRefObject來讓MyAspNetHost跨越兩個Application Domain 
    class MyAspNetHost:MarshalByRefObject
    {
         public void  ProcessRequest( string fileName ,ref string output)
         {
             //利用記憶體當串流。
             MemoryStream memoryStream=new MemoryStream();
             //設定輸出,來將網頁透過這個Stream送到這裡。
             StreamWriter streamWriter = new StreamWriter(memoryStream);
             //自動更新 
             streamWriter.AutoFlush = true;      
             //利用HttpWorkRequest類別,來設定要給Web應用程式的請求。
             //第一個參數是檔案名稱,第二個參數是query,第三個參數是將產生出來的html要透過哪個Stream做寫入至Memory
             HttpWorkerRequest worker = new SimpleWorkerRequest(fileName, "", streamWriter);   
             //正式派發出去執行。
             HttpRuntime.ProcessRequest(worker) ;
             //準備讀取的Stream
             StreamReader streamReader = new StreamReader(memoryStream);
             //移動指標
             memoryStream.Position = 0;
             //從頭讀到尾,並且塞到output裡面去。
             output = streamReader.ReadToEnd();
         }
    }
}

這裡要特別注意,此物件並不是用來傳遞,而是定義了這個物件以後,到時候我們的Web Server就可以來呼叫這個物件的方法,如上所示,我們就可以在Web Server這個類別使用ProcessRequest()這個方法;接下來,我們稍微解釋一下程式碼,基本上前面幾行設定串流模式,並利用記憶體當儲存體;比較需要注意的是這兩行。

 //利用HttpWorkRequest類別,來設定要給Web應用程式的請求。
 //第一個參數是檔案名稱,第二個參數是query,第三個參數是將產生出來的html要透過哪個Stream做寫入至Memory
 HttpWorkerRequest worker = new SimpleWorkerRequest(fileName, "", streamWriter);   
 //正式派發出去執行。
 HttpRuntime.ProcessRequest(worker) ;

我們利用SimpleWorkerRequest類別來產生HttpWorkerRequest這個抽象類別的實體 ( 也就是woker ),這裡有三個參數,第一個參數其實就是檔案名稱,例如Test.aspx,而第二個參數則是query,已就是檔案後面的?xxx=xxxx這種URL的query,第三個則表示未來Web應用程式產出的HTML要利用哪個串流來進行寫入,( 而這邊因為是範例,第二個參數就直接給空白),最後再將這個實體( worker ) 送到HttpRuntime.ProcessRequest來執行,送進去後,就是Web應用程式的事情了,經過一連串的執行運算,最後會產生HTML,並且透過串流寫到記憶體,所以就可以用以下程式碼進行取出。

 //準備讀取的Stream
 StreamReader streamReader = new StreamReader(memoryStream);
 //移動指標
 memoryStream.Position = 0;
 //從頭讀到尾,並且塞到output裡面去。
 output = streamReader.ReadToEnd();

要注意的是output是利用ref的方式,也就是說,到時候Web Server會定義一個output的變數,未來這裡會直接寫回到Web Server的output變數裡面,這樣,就完成了中介的MarshalByRefObject物件。

ApplicationHost.CreateApplicationHost方法

開始撰寫Web Server之前,還有一個東西需要講,那就是ApplicationHost.CreateApplicationHost這個方法,CreateApplicationHost是ApplicationHost的一個靜態方法,他可以幫助我們建立Web應用程式的Application Domain;我們執行程式時,當然會先將Web Server執行起來,也會建立起Web Server的Application Domain,但是Web應用程式呢?,所以我們會在Web Server裡面去建立Web應用程式的Application Domain,而建立的方法就是使用CreateApplicationHost。

//利用CreateApplicationHost方法來建立一個ASP.NET的Application Domain,
//以便執行ASP.NET。
_myAspNetHost = (MyAspNetHost)ApplicationHost.CreateApplicationHost
    (typeof(MyAspNetHost), _virtualDir, _realDir);

如上程式碼,就是建立Web應用程式的Application Domain的寫法,他會帶三個參數,第一個是最難解釋的,先跳過,第二個參數其實就是Web應用程式的根所對應的虛擬路徑,第三個則是實體路徑;接下來我們回來講第一個參數,通常我們會把剛剛寫好的MarshalByRefObject物件放到第一個參數,並且使用typeof來取得MarshalByRefObject的Type這個型別,老實說,這塊我看了很久,最後還去翻Source來查,才知道比較細的細節,以下我們先看一下CreateApplicationHost這個程式碼。

[SecurityPermission(SecurityAction.Demand, Unrestricted=true)]
public static object CreateApplicationHost(Type hostType, string virtualDir, string physicalDir)
{
    if (Environment.OSVersion.Platform != PlatformID.Win32NT)
    {
        throw new PlatformNotSupportedException(SR.GetString("RequiresNT"));
    }
    if (!StringUtil.StringEndsWith(physicalDir, Path.DirectorySeparatorChar))
    {
        physicalDir = physicalDir + Path.DirectorySeparatorChar;
    }
    ApplicationManager applicationManager = ApplicationManager.GetApplicationManager();
    string appId = (virtualDir + physicalDir).GetHashCode().ToString("x");
    return applicationManager.CreateInstanceInNewWorkerAppDomain(hostType, appId, VirtualPath.CreateNonRelative(virtualDir), physicalDir).Unwrap();
}

上面在敘說CreateApplicationHost這個方法,我們其實可以看到,傳進來的Type型別,又傳入到CreateInstanceInNewWorkerAppDomain裡面去,所以再往下看吧。

internal ObjectHandle CreateInstanceInNewWorkerAppDomain(Type type, string appId, VirtualPath virtualPath, string physicalPath)
{
    IApplicationHost appHost = new SimpleApplicationHost(virtualPath, physicalPath);
    HostingEnvironmentParameters hostingParameters = new HostingEnvironmentParameters {
        HostingFlags = HostingEnvironmentFlags.HideFromAppManager
    };
    return this.CreateAppDomainWithHostingEnvironmentAndReportErrors(appId, appHost, hostingParameters).CreateInstance(type.AssemblyQualifiedName);
}

到這邊,直接跳到最後一行,我們可以看到type.AssmblyQualifiedName,其實這是取出type的詳細資訊,也就是MarshalByRefObject的NameSpace、版本、等等的詳細資訊;這邊如果再繼續查下去,還有非常多層,我就跳過不講了,但至少我們發現了關鍵,也就是說,這邊轉成Type是為了讓Web應用程式的Application Domain在runtime時,動態載入MarshalByRefObject這個物件,所以在runtime的時候,程式會去尋找GAC和網站底下的bin目錄,並將之載入,所以需要知道MarshalByRefObject這個物件的詳細資訊,所以使用Type來取得詳細資訊;到這邊,我們知道了CreateApplicatinHost如何使用,我們就可以開始寫Web Server了。

撰寫Web Server

接下來就是整段的Web Server程式碼,下面比較簡單的都註解了,基本上看一下應該都可以看得懂。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.IO;
using System.Threading;
using System.Web;
using System.Web.Hosting;
using System.Net;

namespace AspNetSimulator
{
    class WebHandle
    {
        //繼承了MarshalByRefObject的MyAspNetHost,
        //負責處理We Server 和 Asp.net Application Domain之間的通訊
        private MyAspNetHost _myAspNetHost;
        private TcpListener _tcpListener;    //監聽用
        private FileStream _fileStream;      //利用串流處理要求。
        private string _virtualDir;
        private string _realDir;

        public WebHandle(string virtualDir, string realDir)
        {
            this._virtualDir = virtualDir;
            this._realDir = realDir;
        }

        //啟動服務
        public void StartService()
        {
            try
            {
                IPEndPoint address = new IPEndPoint(IPAddress.Loopback, 8080);
                _tcpListener = new TcpListener(address);
                //啟動監聽
                _tcpListener.Start();   
                Console.WriteLine("啟動服務...");
                //利用CreateApplicationHost方法來建立一個ASP.NET的Application Domain,
                //以便執行ASP.NET。
                _myAspNetHost = (MyAspNetHost)ApplicationHost.CreateApplicationHost
                    (typeof(MyAspNetHost), _virtualDir, _realDir);
              //啟動服務
                WebServiceStart();
            }
            catch (NullReferenceException)
            {
                Console.WriteLine("NullReferenceException throwed!");
            }
        }

        //主要服務
        public void WebServiceStart()
        {
            while (true)
            {
                //建立連線
                TcpClient tcpClient = _tcpListener.AcceptTcpClient();
                Console.WriteLine("連線建立...");
                //取得網路串流
                NetworkStream networkStream = tcpClient.GetStream();
                //設定使用UTF-8
                Encoding utf8 = Encoding.UTF8;
                byte[] request = new byte[4096];
                //讀取請求,並塞到request裡面,此方法會傳回長度。
                int length = networkStream.Read(request, 0, 4096);
                //轉換成人類能看得懂的字串。
                string requestString = utf8.GetString(request, 0, length);

                string output = "";
                //這裡是關鍵,
                //簡單的說,就是WebHandle這個Application Domain 透過 _myAspNetHost 來和
                //ASP.NET Application Domain傳遞訊息。
                //( 這裡因為是Demo,所以就直接指定為Test.aspx網頁了 )
                //而使用ref來將ASP.NET Application Domain的資料塞回這裡。
                _myAspNetHost.ProcessRequest("Test.aspx", ref output);
                //以下只是簡單的將訊息送出。
                string statusLine = "HTTP/1.1 200 OK\r\n";
                byte[] outputStatusLineBs = utf8.GetBytes(statusLine);
                byte[] outputBodyBs = utf8.GetBytes(output);
                string responseHeader = string.Format("Content-Type: text/html;charset=UTF-8\r\nContent-Length:{0}\r\n",outputBodyBs.Length);
                byte[] outputResponseHeaderBs = utf8.GetBytes(responseHeader);
                networkStream.Write(outputStatusLineBs, 0, outputStatusLineBs.Length);
                networkStream.Write(outputResponseHeaderBs, 0, outputResponseHeaderBs.Length);
                networkStream.Write(new byte[]{13,10},0,2);
                networkStream.Write(outputBodyBs, 0, outputBodyBs.Length);
                tcpClient.Close();
            }
        }
    }
}

我們會在_tcpListerer.start()後,建立Web應用程式的Application Domain,來讓Web應用程式準備運作,然後進入WebServiceStart方法,利用無窮迴圈來讓服務不中斷,而當程式讀到這行時,就會暫停,直到有請求進來,並繼續執行下去;另外WebServiceStart可以使用執行序來進行多執行序的處理,這裡是簡單的Demo,就直接這樣寫了。

TcpClient tcpClient = _tcpListener.AcceptTcpClient();

然後就進行串流的讀取,將請求的部分進行處理,如果寫的複雜一點,可能也還需去判斷要求的是哪個檔案,或是有沒有query等等的處理。( 所以沒事不要自己寫Web Server… )最後到下面這行,這邊就是透過繼承了MarshalByRefObject的物件,並呼叫此物件的方法,如上所說,經過一連串的執行,就會把html資料吐回到output裡面。

//這裡是關鍵,
//簡單的說,就是WebHandle這個Application Domain 透過 _myAspNetHost 來和
//ASP.NET Application Domain傳遞訊息。
//( 這裡因為是Demo,所以就直接指定為Test.aspx網頁了 )
//而使用ref來將ASP.NET Application Domain的資料塞回這裡。
_myAspNetHost.ProcessRequest("Test.aspx", ref output);

最後,我們去把這些資料輸出回瀏覽器,就完成了這個平常簡單的一次請求…

還有一小段進入點!

另外,有沒有發現上面全部都是類別,完全沒有進入點,其實我是使用了Console專案來撰寫,所以下面補上程式的進入點,於是Web Server (偽) 就完成了=w=。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace AspNetSimulator
{
    class ServerSimulator
    {
        static void Main(string[] args)
        {
            WebHandle webHandle = new WebHandle("/", "C:\\My\\TestWeb");
            webHandle.StartService();
        }
    }
}

寫了那麼多,其實也只是ASP.NET的開頭,也希望能讓大家對ASP.NET的一些基礎能更加瞭解!

2011年10月4日

開發人應該學的,用Visual Studio開發SQL - 錯誤訊息 - "偵測到資料列。結構描述更新即將終止,因為可能造成資料遺失。"

這個案例是這樣的,本機的結構上,Customers裡面沒有Fax2,但是正式DB上有Fax2,而想利用Visual Studio更新此Table時產生的錯誤;以下是差異比較的圖,我們要將正式DB的Fax2刪除。

image

但是按下更新目標的時候,卻產生以下錯誤:

SQL72014: .Net SqlClient Data Provider: 訊息 50000,層級 16,狀態 127,行 6 偵測到資料列。結構描述更新即將終止,因為可能造成資料遺失。

image

如字面上所敘述,簡單的講,就是正式DB的這個Table已經有資料了,所以不給我們刪除這個欄位,其實這也是不錯的防呆機制。

原本以為有地方可以設定略過這種機制,但似乎沒有 ( 如果未來有找到,再補上 ),所以我們用另外一種方式來處理,也就是產生指令碼的方式。image

我們可以發現Script的部分有這一行,其實就是這一行會檢查是否有無資料,我們只要把這行刪除,或是註解掉。

/*
正在卸除資料行 [dbo].[Customers].[Fax2],因此可能導致資料遺失。
*/

IF EXISTS (select top 1 1 from [dbo].[Customers])
    RAISERROR ('偵測到資料列。結構描述更新即將終止,因為可能造成資料遺失。', 16, 127) WITH NOWAIT

然後執行這段Script ( 按下綠色箭頭或是執行查詢都可以 ),就可以順利刪除了。

開發人應該學的,用Visual Studio開發SQL(SQL Server Data Tools) - 不使用DataBase Project的開發模式

2012/4/5 從SQL Server Developer Tools,Codename “Juneau” CTP3版本更新成SQL Server Data Tools – DataBase Project

除了上一篇說的,利用DataBase Project進行開發外,其實如果想直接對SQL Server做動作,SQL Server Data Tools也支援,不過這部分比較沒甚麼好提的,但畢竟是一系列的,所以也在這邊稍微解說一下。

其實不使用DataBase Project的方式和SSMS做的事情差不多,都是用撰寫SQL後,直接對SQL Server產生影響,那有人就會想問說,那和SSMS有甚麼不一樣!?大家可以參考下面兩張圖,左邊是SSDT,右邊是SSMS,可以發現,其實對於專職於管理SQL Server的DBA來說,還是會比較偏向使用SSMS,因為SSMS的功能比較強大,而對於開發SQL程式的程式設計師來說,其實SSDT就夠了。

imageimage

如果是在SSDT裡面的Table下,按下右鍵,並按下"檢視表設計工具"會出現下圖。

image

如果是SSMS裡面的Table下,按下右鍵,並按下"設計",會出現下圖。

image

其實可以發現,SSDT裡面,還是會有在本機開發的感覺,而利用"更新",或是"產生指令碼"的方式去對DB做變動,但如果是SSMS裡面,就會讓然感覺,我們已經連上去了,做的任何動作,都像在Server端上面做。

如果在SSDT裡面按下更新,SQL Server Data Tools也會自動的提供一些資訊給你,當然,也可以選擇產生指令碼做進一步的調整,也可以很有魄力的按下更新資料庫。

image

基本上這是屬於不使用DataBase Project的方法,很簡單,在這邊也給大家參考參考。

開發人應該學的,用Visual Studio開發SQL - 錯誤訊息 - "你必須解決驗證錯誤後才能寫入更新"

今天在講這個主題的時候,比較資料庫的時候,給我很沒面子的發生錯誤,"你必須解決驗證錯誤後才能寫入更新…",如下:

image

去看了錯誤,其實也很簡單,因為沒有用到重構功能,所以直接改變欄位名稱的時候,發生相依性的錯誤,也就是說,很多View或是SP找不到原本的欄位名稱。

image

其實資料庫比對的功能和正式發佈的功能都會先去建置偵錯看看,就好像一般的程式一樣,所以當我沒有建置過,而直接進行資料庫的比對,當Visual Studio發現錯誤的時候,就不會給你去比較資料庫,其實,這也是非常合理的,就像無法執行的程式,還給你發佈,那還得了。所以提醒大家,也別忘了在發佈或是比對資料庫前,按下綠色小箭頭,建置偵錯看看,看看有沒有問題,雖然說,發佈的時候,也是會進行此動作,但還是提早發現比較好。

另外,補充一點,並不是所有綠色小箭頭都是針對專案,如果是Script視窗的綠色小箭頭其實是直接針對正式DB去做偵錯與寫入的動作了,如下圖:

image

最後,如果還有Demo,希望不要再搞出這種問題了= =。

不要使用img標籤來當作button!

這個問題已經遇到不只一次,今天又被我遇到了…之前也寫過類似的,但比較著重於ASP.NET MVC的部分 ( 請參閱這裡 ),所以這此我要把這個問題特別拉出來寫。

<a href="#">
<img src="" alt="send" name="Test" width="150"
height="37" border="0" id="Test"
onclick="send();" />
</a>

以上程式碼是滿常見的一種寫法,就是使用img標籤搭配上onclick來達到按下此圖就會有某種效果的作法,簡單的說,就是把img標籤當作按鈕來使用,然後因為滑鼠移動到img上面,並不會改變滑鼠的鼠標,所以外面又包了一個a標籤…

這個做法看起來沒什麼 ( 但問題超大… ),但對於不熟悉HTML的設計師來講,這也算是達到了想要的結果,也不能說這就是網頁設計師的錯之類的,畢竟經歷過瀏覽器亂七八糟的歷史,有各種方法,也不覺得奇怪了,但未來的標準越來越重要,所以這樣的寫法,也必須要修正一下。

首先,這樣寫法的問題,其實還不一定會產生問題,像我這次碰到的例子,就是這種寫法加上Firefox再加上jQuery和jQuery UI,都好好的沒發生啥問題,但是同樣組合,運作在IE9上,恭喜!,jQuery就會發生錯誤。

就如我之前的那篇MVC 2和AJAX問題一樣,這種寫法,也不是一定會產生問題,但當產生問題的時候,也讓人往往不知所措。

那到底怎樣寫呢?

<input type="image" src=""  alt="send" name="Test" id="Test"
onclick="send();" />

是的,請使用input標籤,然後配上image屬性,這才是比較好的做法,所以不要再使用img標籤當作Button了,img標籤就是顯示圖片用,就是這樣單純。

最後,至於要改變鼠標,也請使用CSS的方式來修改,也不要再濫用a標籤了,a標籤會哭的喔…

2011年10月3日

開發人應該學的,用Visual Studio開發SQL( SQL Server Data Tools ) - 快照功能

2012/4/5 從SQL Server Developer Tools,Codename “Juneau” CTP3版本更新成SQL Server Data Tools – DataBase Project

前面寫了一些開發者必學的超強功能,今天再來講一下新功能"快照功能",聽到快照,大家應該會直接聯想到VM的快照,沒錯,這個功能就很像VM的快照,會把目前的資料庫結構( 含View、SP ),複製一份起來,未來不但可以進行差異比較,也很方便複寫回去。

要使用這個功能其實很簡單,只要在Database Project的名稱處,按下滑鼠右鍵,選擇快照集專案即可。( 只有Database Project裡面才能使用此功能喔 )

image

按下後,我們就可以發現專案底下多出來一個.sqlx的檔案,這就是快照產生的。

image

然後就可以利用結構化比較進行差異比較,也可以輕鬆地還原。

image

其實快照功能我在Demo的時候還滿好用的,我會先把原始的結構快照下來,然後進行Demo,Demo完後,我再利用快照恢復就可以了。

老實說這篇也超級短,但是的確也是一個非常不錯的功能喔!