2016年4月17日 星期日

[ASP.NET WebForm] 遞迴搭配多型實務應用

遞迴本身就是一種把問題切割成更小的問題求解

當問題切割到最小求得這個小問題的解之後

再將這些小問題的解傳回來組合成最終解答

一般經典的例子就是求費氏數列

遞迴也像俄羅斯娃娃一樣

一個娃娃裡面有一個娃娃,這個娃娃裡面又有一個娃娃…


實務上遇到一個問題

如果一個畫面的控制項某些一開始載入的時候就要設為ReadOnly

那麼應該怎麼做?

最直接的方法當然就是Page_Load()的時候開始寫

TextBox1.ReadOnly=true;
TextBox2.ReadOnly=true;
TextBox3.ReadOnly=true;
TextBox4.ReadOnly=true;

以上只有四個物件需要設定成ReadOnly

那如果有100個物件需要設定呢?

而且有不同的物件也要做不同處理呢?

當然不是真的去寫100行

身為程式設計師就要用程式設計師的方法去偷懶~

首先先聊聊WebForm的控制項特性

WebForm的控制項其實本身就是一種容器的概念

也就是一個控制項裡面可能還有其他控制像

其中的控制項裡面可能又有其他的控制項…

就像上面提到的俄羅斯娃娃一樣

所有控制項的父類別來自於Control

所以從多型(Polymorphism)的觀點來看

TextBox是Control

DropDownList也是Control

如果我們能夠取得控制項中的控制項

又能分辨出這個控制項是誰這問題就解決了

解法如下

private void SetDisable(ControlCollection Controls)
    {
        foreach (var obj in Controls)
        {
            if (obj is Control && (obj as Control).HasControls())
            {
                SetDisable((obj as Control).Controls);
            }
            if (obj is TextBox && (obj as TextBox).ID != txtMemo.ID && (obj as TextBox).ID != txtDocWord.ID)
            {
                (obj as TextBox).ReadOnly = true;
                continue;
            }
            if (obj is DropDownList && (obj as DropDownList).ID != ddlModifyCategory.ID)
            {
                (obj as DropDownList).Enabled = false;
                continue;
            }
        }
    }

SetDisable這個方法有一個傳入的引數型態就是ControlCollection

在Control類別底下就有Controls這個屬性

他代表這個物件中包含的其他控制項物件

ControlCollection就是代表了一個控制項的集合

所以我們能用foreach (var obj in Controls)去取得其中的全部子控制項物件

並且用C#的隱含型別去接從Controls得到的物件

當然也可以用Object型別去接

接下來這段也就是多型跟遞迴應用的部份

if (obj is Control && (obj as Control).HasControls())
{
    SetReadOnly((obj as Control).Controls);
}

obj is Control其中的is是C#用來檢查左邊的物件是否是右邊的型別

在此就是判斷說你取得的物件是否是Control類別的物件

如果他是Control類別的物件

那就代表他一定有Controls這個屬性

Controls本身也有提供HasControls()判斷裡面還有沒有控制項並回傳true或false

如果是Control類別的物件而且HasControls()為true就會進入該判斷式

並且再呼叫一次SetDisable傳入該物件的Controls集合

繼續檢查這個集合中的控制項是否還有子控制項

如果HasControls()為false表示沒有子控制項了

就繼續下面流程

下面流程主要也只是判斷這個物件是不是TextBox跟DropDownList

因為繼承Control的類別很多

但我們要設定的也只有TextBox跟DropDownList

因為在is的部份就已經確定是否為TextBox或DropDownList

所以後面使用as轉型一定不會失敗

之後依照型別做相對應的設定

2014年9月18日 星期四

[ASP.NET]GridView中的控制項修改其他行控制項Part2

上一篇研究如何知道目前是哪一列的的控制項被觸發

好讓我們可以去修改同一列的內容

雖然提出了一個方式

但可以解決這問題的方式果然不只一種

論壇上topcat跟MIS2000提出用NamingContainer來取得我在哪個控制項中(就是GridViewRow)

protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e)
{
    DropDownList ddl = sender as DropDownList;
    GridViewRow gv = ddl.NamingContainer as GridViewRow;
    Label lb = gv.FindControl("Label1") as Label;
    lb.Text = ddl.SelectedValue;
}


不過讓我覺得神奇是Allen提出來的方式

protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e)
{
    var ddl = sender as DropDownList;
    var lb = ddl.FindControl("Label1") as Label;
    lb.Text = ddl.SelectedValue;
}

重點在var lb = ddl.FindControl("Label1") as Label;這段

我本來以為FindControl是專門用來找某個控制項中的子控制項(剛好跟NamingContainer相反)

但居然可以用同一階的控制項找到其他控制項

這可就神奇了~

因為依照MSDN上的解釋FindControl的功能「以指定的id去搜尋當前容器"內"的伺服器控制項。」

既然是內沒想到連隔壁的都可以找到

看來我對FindControl特性還不夠熟

有好好研究的必要!

參考文獻
使用 NamingContainer 屬性決定控制項的命名容器
Control.NamingContainer 屬性
HOW TO:存取 Web 伺服器控制項命名空間的成員
Control.FindControl 方法 (String)
GridView中取得目前使用的控制項所在的列

2014年9月17日 星期三

[ASP.NET]GridView中的控制項修改其他行控制項

在GridView中你可能有一行有DropDownList

你希望選擇這一行的DropDownList可以讓另外一行的Label帶出資料

像是選擇幣別然後同一列的其他行帶出匯率之類的

這個功能在SelectedIndexChanged就能辦到

但會有個問題

我怎麼知道是哪一列的DropDownList被異動?

如果我不知道是哪個DropDownList被異動

我又怎麼知道要去修改哪一列的內容

這個問題解法似乎很多

目前我只想到這種解法

protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            DataTable dt = new DataTable();
            dt.Columns.Add(new DataColumn("No", typeof(string)));

            for (int i = 0; i < 5; i++)
            {
                DataRow dr = dt.NewRow();
                dr["No"] = i.ToString();
                dt.Rows.Add(dr);
            }
            GridView1.DataSource = dt;
            GridView1.DataBind();
        }
    }
    protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e)
    {
        DropDownList ddl = sender as DropDownList;
        int index = Convert.ToInt32(ddl.Attributes["index"]);
        Label txt = GridView1.Rows[index].FindControl("Label1") as Label;
        txt.Text = ddl.SelectedValue;
    }
    protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
    {
        if (e.Row.RowType == DataControlRowType.DataRow)
        {
            DropDownList ddl = e.Row.FindControl("DropDownList1") as DropDownList;
            ddl.Attributes.Add("index", e.Row.RowIndex.ToString());
        }
    }

簡單說就是利用每個控制項都有的Attributes

用Attributes取個編號到時候你再取出當下這個DropDownList的編號就可以知道是那一列了

如果是Button可以用RowCommand的事件知道是哪個按鈕被按

印象中應該還有其他解決方式

有研究出來再來分享吧~

2014年8月10日 星期日

[ASP.NET]如何取消Button控制項的PostBack

ASP.NET多數控制項都可以選擇是否觸發AutoPostBack

但偏偏Button控制項沒這東西讓你選,預設就是有

後來在ASP.NET的Button控制項上使用jQuery的toggle()

控制標籤顯示或隱藏

但是Button一按下去原本隱藏的標籤是有顯示了

但馬上又隱藏………

改CausesValidation="False"(是否觸發驗證)

跟UseSubmitBehavior="False"(是否為submit的按鈕)

都不行…

因為都是按下按鈕後畫面閃一下就收起來

而且在Page_Load()設定中斷點也有進到裡面

所以推斷應該是PostBack搞出來的問題

查了有無方法不讓Button控制項觸發PostBack

一開始有找到這個

Button.Attributes.Add("onclick", "event.returnValue=false;");

不過還是怪怪的…Chrome可以解決,IE跟FireFox一樣不行

可是到後來又不行了(是七月的關係嗎?)

後來終於找到在IE、FireFox、Chrome都可以取消PostBack的方法

Button.Attributes.Add("onclick", "return false;");

就很乾脆的回傳false就不會觸發PostBack

但要注意的是如果這樣寫的話Button就真的完全不會執行所Binding的事件

我是因為只拿來當開關所以沒差

參考文獻
[1] http://stackoverflow.com/questions/7547945/disable-postback-at-click-on-a-button
[2] http://stackoverflow.com/questions/683746/how-to-disable-postback-on-an-asp-button
[3] http://www.dotblogs.com.tw/yc421206/archive/2009/04/22/8125.aspx
[4] http://www.blueshop.com.tw/board/show.asp?subcde=BRD20080729150920PJW
[5] http://cate-taiwan.blogspot.tw/2008/08/aspnet-button-postback.html

2014年7月31日 星期四

[ASP.NET]Global.asax功能筆記

在ASP.NET專案底下有個Global.asax的檔案

該檔案可以在系統發生某些事件的時候被執行

個人用到的是捕捉例外的功能

在此做個筆記

<%@ Application Language="C#" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.Security" %>
<script RunAt="server">

    void Application_Start(object sender, EventArgs e)
    {
        // 應用程式啟動時執行的程式碼
    }

    void Application_End(object sender, EventArgs e)
    {
        //  應用程式關閉時執行的程式碼

    }

    void Application_Error(object sender, EventArgs e)
    {
        // 發生未處理錯誤時執行的程式碼
        HttpServerUtility server = HttpContext.Current.Server;
        HttpRequest request = HttpContext.Current.Request;
        HttpSessionState session = HttpContext.Current.Session;
        HttpBrowserCapabilities brower = HttpContext.Current.Request.Browser;
        Exception ex = server.GetLastError();
        System.Net.IPAddress ServerIP = new System.Net.IPAddress(System.Net.Dns.GetHostByName(System.Net.Dns.GetHostName()).AddressList[0].Address);

        StreamWriter sw = null;

        try
        {
            if (ex.InnerException != null)
            {
                ex = ex.InnerException;
            }

            string filePath = string.Format(@"{0}Log\err_log_" + DateTime.Now.ToString("yyyyMMdd") + ".txt", server.MapPath("~/"));

            sw = File.Exists(filePath) ? new StreamWriter(filePath, true) : File.CreateText(filePath);

            sw.WriteLine("----------Strat----------");
            sw.WriteLine("----------[目前時間:" + DateTime.Now.ToString() + "]----------");
            sw.WriteLine("事件發生網頁網址:" + request.Url);
            sw.WriteLine("事件發生路徑:" + request.Path);
            sw.WriteLine("例外狀況訊息:" + ex.Message);
            //事件發生網頁網址

            sw.WriteLine("例外堆疊:" + ex.StackTrace);

            sw.WriteLine("造成錯誤的程式名稱:" + ex.Source);

            //使用者名稱
            if (session != null)
                sw.WriteLine("使用者名稱:" + Session["UserID"].ToString());

            sw.WriteLine("識別系統別:" + request.UserHostAddress);

            sw.WriteLine("IP:" + ServerIP.ToString());

            sw.WriteLine("使用瀏覽器:" + brower.Type);

            sw.WriteLine("是否支援Cookie:" + (brower.Cookies ? "是" : "否"));

            sw.WriteLine("伺服器名稱:" + request.ServerVariables["SERVER_NAME"]);

            sw.WriteLine("----------END----------");

        }
        catch (HttpException HE)
        {
            throw HE;
        }
        catch (ArgumentNullException ANE)
        {
            throw ANE;
        }
        catch (ArgumentException AE)
        {
            throw AE;
        }
        catch (UnauthorizedAccessException UAE)
        {
            throw UAE;
        }
        catch (DirectoryNotFoundException DNFE)
        {
            throw DNFE;
        }
        catch (IOException IOE)
        {
            throw IOE;
        }
        catch (SecurityException SE)
        {
            throw SE;
        }
        finally
        {
            sw.Close();
            sw.Dispose();
        }
    }
</script>


Application_Error可以捕捉網站中的例外

但是如果該例外在網頁中就已經被捕捉

就不會被Application_Error給catch到

所以簡單說Application_Error專門接漏網之魚

這很適合用在你在本機執行沒問題

丟到IIS莫名其妙掛掉的情況

還有一些例外情況但被忽略掉的情形

上面範例就是當發生例外時

會取得該例外物件

然後描述該例外的情形

最好用的就是HttpContext.Current.Request的Url跟Path屬性

他可以指出當前的Request是來自於哪個網址跟路徑

這在IIS上執行有bug時又不曉得bug從何而來很好用

其餘的HttpContext.Current.Server、HttpContext.Current.Session、HttpContext.Current.Request.Browser

都可以讓你取得當下相關資訊並且在Application_Error寫入txt或是資料庫

方便你事後分析問題點

參考文獻
[1] http://www.dotblogs.com.tw/mis2000lab/archive/2008/04/28/3526.aspx
[2] http://sharebody.com/list.asp?id=50684
[3] http://msdn.microsoft.com/zh-tw/library/vstudio/2027ewzw%28v=vs.100%29.aspx
[4] http://www.webjx.com/htmldata/2006-08-14/1155516831.html
[5] http://myprogramlog.blogspot.tw/2013/09/globalasax-applicationbeginrequest.html

2014年7月2日 星期三

[C#]Debugger會呼叫ToString()的問題

為了紀念一下我逝去的青春小鳥(浪費了我一整個下午)

來紀錄一下這次遇到的事件

簡單說我寫了一個組合SQL字串的class

其中裡面有InsertString、DeleteString、UpdateString、WhereString等StringBuilder的物件紀錄執行的動作

當中我override ToString()這個來自System.Object的方法

來輸出組合並輸出的字串

詭異的事情就此發生

在Visual Studio偵錯模式底下執行看程式變化時

有用過的人應該知道可以按F10或F11一步一步執行

其中你游標指到變數還可以看到變數的內容

後來我發現每指向變數一次變數(StringBuilder)的內容就會改變一次

而且用ToString()輸出後的確是有改變

之後我嘗試在ToString()中回傳前指派InsertString、DeleteString、UpdateString、WhereString都為null

沒想到居然會發生參照未指定物件的錯誤

但問題是我根本還沒呼叫ToString()這個方法

怎麼「看起來」好像有去呼叫ToString()

難道是我學藝不精?還是C#有什麼神奇魔力…?

找了半天也跟別人討論

最後才發現這個不曉得算不算bug的問題來源

一般偵錯模式底下執行時我們游標指到變數會顯示的是這個物件完整的class name

但其實這個內容是呼叫ToString()來的

可是ToString()又已經被我Override過了

所以就會發生我遇到明明沒呼叫ToString()卻會執行ToString()內容的靈異事件了

這個問題好像一直到Visual Studio 2012都還有

難道微軟不認為這是bug嗎?

後來我有嘗試方法用new但還是不行

他看來就是一定會去執行這個物件的ToString()不管你是override還是new

看起來他並非去copy出一個新物件

再去呼叫這個新物件的ToString()

所以要解決這個問題你只能取別的名字避免他莫名其妙偷偷去呼叫ToString()

可以參考以下兩篇也是苦主的文章

http://mocheng.wordpress.com/2009/06/06/%E5%88%AB%E8%AE%A9tostring%E5%87%BD%E6%95%B0%E6%94%B9%E5%8F%98%E5%AF%B9%E8%B1%A1%E7%9A%84%E7%8A%B6%E6%80%81/
http://www.xuebuyuan.com/130963.html

2014年7月1日 星期二

[Oracle SQL]使用update指定修改日期欄位

最近遇到日期轉換錯亂的問題

因為程式中在還沒完全改成民國年的情況下就執行

會導致儲存到Oracle上之後

出現3925年(1911+2014)這種結果

但要把時間改回2014年資料有上千筆當然不可能一個一個去改

當然還是要靠update的指令

但問題來了

如果你直接去用to_date改日期

最後的結果可能是變成2014/07/01 00:00:00

原本的時分秒可能因為沒指定被蓋掉

那麼要如何只改年不動到月日時分秒呢?

後來我問到的方法如下

update Login k set k.date=ADD_MONTHS(k.date,-1911*12)
where to_char(k.date,'yyyy')='3925';

這段的意思是說我將現有的時間減掉1911*12個月

至於where的部份必須要用to_char取得年

因為如果是用to_date的話會變成3925/07/01 00:00:00這種結果

當然你的時分秒不會剛好是00:00:00

所以也不會找到符合的結果