目前分類:.Net隨手記 (47)

瀏覽方式: 標題列表 簡短摘要

  上一篇寫了MTOM Server端的部份,這次要寫的就是Client端的程式要怎麼跟Server互動,也就是最後一篇,廢話不多說,馬上進入正題,Client端的程式跟Server端一樣,要用WSE的Configuration Tool來設定,而Web Service是存在Web.Config,那Client端的WinForm就是存在app.config


?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
      <section name="MTOMClient.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
    </sectionGroup>
    <sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
      <section name="MTOMClient.MTOMSet" type="System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
    </sectionGroup>
    <section name="microsoft.web.services3" type="Microsoft.Web.Services3.Configuration.WebServicesConfiguration, Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
  </configSections>
  <applicationSettings>
    <MTOMClient.Properties.Settings>
      <setting name="MTOMClient_MTOMWS_ServiceMTOM" serializeAs="String">
        <value>http://localhost:2448/MTOMWebService/ServiceMTOM.asmx</value>
      </setting>
    </MTOMClient.Properties.Settings>
  </applicationSettings>
  <userSettings>
    <MTOMClient.MTOMSet>
      <setting name="FolderPath" serializeAs="String">
        <value>c:\temp</value>
      </setting>
    </MTOMClient.MTOMSet>
  </userSettings>
  <system.web>
    <webServices>
      <soapExtensionImporterTypes>
        <add type="Microsoft.Web.Services3.Description.WseExtensionImporter, Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
      </soapExtensionImporterTypes>
    </webServices>
    <compilation>
      <assemblies>
        <add assembly="Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
      </assemblies>
    </compilation>
  </system.web>
  <microsoft.web.services3>
    <messaging>
      <mtom clientMode="On" />
    </messaging>
  </microsoft.web.services3>
</configuration>

因為有Tool可以幫忙,裡面的設定就不用一個一個打上去了.


開始步驟跟Web Service的做法一樣,開啟app.config,並做以下的設定.






做完上述設定,app.config就大致上完成了,接著下來就要要將Web Service加入參考,原本這是個很平常的動作,但有一點地方要注意一下,加入後的Web Service,要看一下它的Reference.cs.



反藍的那一段必需為Microsoft.Web.Services3.WebServicesClientProtocol


系統預設會是System.Web.Services.Protocols.SoapHttpClientProtocol


這點要注意到,它會導致錯誤.



這樣就完成大部份的設定工作,接下來就是Coding部份,在我們new Web Service時,有個屬性要記得設,RequireMtom = true;



接下來就是呼叫Web Service的Method來進行上/下載,其實設定到這裡,已經完成了大半,而呼叫Web Service的動作就像我們在呼叫一般的function一樣,沒有什麼特別的動作,比如說下載.



如此就可以從Web Service取得到檔案的buffer,再將buffer用FileStream寫到檔案即可,上傳也是一樣,用FileStream讀取本端的檔檔到buffer,再呼叫Web Service把這Buffer給上傳過去即可.


在使用WSE這段時間以來,發現它還不錯用,在用SSL時有遇到一點小問題,但後來也是解決了,但WSE 3.0後就沒有再出下一版了,因為它整合到WCF內,未來有機會再來看看WCF版的傳檔.


這 次程式碼的介紹部份不多,因為有點小長,主要是因為用backgroundworker,及浮動式依傳輸速率去調整Buffer的長度,如果走固定又不用 backgroundworker,程式碼就簡單許多,所以這次放上整個Source Code,給有興趣的朋友參考,這個就包含Web Service及Client端的程式.




原始碼下載 :


MTOM.rar

jeffyeh 發表在 痞客邦 留言(0) 人氣()

  上篇大致上說明了MTOM的相關資訊,而這篇將開始講程式實作的部份,由於這程式有Client及WS兩端的程式,所以這次將以WS端的來講,而這Client端的AP將會長的像下面的樣子,一共有四個功能,取得取單/上傳/下載,還有一個檔案驗證功能.



Web.Config的設定如下:


01 <?xml version="1.0"?>
02 <configuration>
03  <configSections>
04      <section name="microsoft.web.services3" type="Microsoft.Web.Services3.Configuration.WebServicesConfiguration, Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
05  </configSections>
06  <appSettings>
07      <add key="UploadPath" value="Upload"/>
08  </appSettings>
09  <connectionStrings/>
10  <system.web>
11     <compilation debug="true">
12    <assemblies>
13        <add assembly="Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
14    </assemblies>
15      </compilation>
16      <authentication mode="Windows"/>
17      <webServices>
18    <soapExtensionImporterTypes>
19        <add type="Microsoft.Web.Services3.Description.WseExtensionImporter, Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
20    </soapExtensionImporterTypes>
21    <soapServerProtocolFactory type="Microsoft.Web.Services3.WseProtocolFactory, Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
22      </webServices>
23     <httpRuntime maxRequestLength="16384"></httpRuntime>
24  </system.web>
25  <microsoft.web.services3>
26      <messaging>
27    <mtom serverMode="always"/>
28     </messaging>
29  </microsoft.web.services3>
30 </configuration>
31

從這個config裡可以看到幾行不是WSE就是MTOM,這些值要怎麼來?我們可以用WSE的Configuration Tool來設定,程式剛開啟時像下面一樣是空白的.



這時從File -> Open,開啟web.config這個檔案,就可以開始設定的動作(Client端的設定也會用到此工具,但設定不太相同),以下的設定就以畫面的方式直接顯示.



(其它用預設值,不過有興趣的人,可以再針對每個細項設定去瞭解)



設定完再把它給存檔即可,那麼Web.Config就會設好WSE MTOM的部份.


 


接下來的Web Service的部份程式碼如下 :


public class ServiceMTOM : System.Web.Services.WebService
{
    private string UploadPath;
    public ServiceMTOM()
    {
        // 檢查上傳路徑是否存在
        string uploadConfigSetting = ConfigurationManager.AppSettings["UploadPath"].ToString();
        if (Path.IsPathRooted(uploadConfigSetting))
        {
            UploadPath = uploadConfigSetting;
        }

        else
        {
            UploadPath = Server.MapPath(uploadConfigSetting);
        }


        if (!Directory.Exists(UploadPath))
        {
            Directory.CreateDirectory(Server.MapPath(uploadConfigSetting));
        }

    }


    #region Upload
    /// <summary>
    /// 上傳檔案
    /// </summary>
    /// <param name="FileName">檔案名稱 string</param>
    /// <param name="buffer">傳輸量 long</param>
    /// <param name="Offset">檔案起始 long</param>
    [WebMethod]

    public void AppendChunk(string FileName, byte[] buffer, long Offset)
    {
            string FilePath = Path.Combine(UploadPath, FileName);

            if (Offset == 0)    // 如果起始為0,則產生一個新檔.
            {
                File.Create(FilePath).Close();
            }

            else if (!File.Exists(FilePath))
            {
                CustomSoapException("檔案不存在", String.Format("檔案 : {0} 不存在", FilePath));
            }

            long FileSize = new FileInfo(FilePath).Length;
            if (FileSize < Offset)
            {
                CustomSoapException("檔案Offset錯誤", "Offset大於檔案長度");
            }


            //開啟檔案寫入.
            try
            {
public class ServiceMTOM : System.Web.Services.WebService
{
    private string UploadPath;
    public ServiceMTOM()
    {
        // 檢查上傳路徑是否存在
        string uploadConfigSetting = ConfigurationManager.AppSettings["UploadPath"].ToString();
        if (Path.IsPathRooted(uploadConfigSetting))
        {
            UploadPath = uploadConfigSetting;
        }

        else
        {
            UploadPath = Server.MapPath(uploadConfigSetting);
        }


        if (!Directory.Exists(UploadPath))
        {
            Directory.CreateDirectory(Server.MapPath(uploadConfigSetting));
        }

    }


    #region Upload
    /// <summary>
    /// 上傳檔案
    /// </summary>
    /// <param name="FileName">檔案名稱 string</param>
    /// <param name="buffer">傳輸量 long</param>
    /// <param name="Offset">檔案起始 long</param>
    [WebMethod]

    public void AppendChunk(string FileName, byte[] buffer, long Offset)
    {
            string FilePath = Path.Combine(UploadPath, FileName);

            if (Offset == 0)    // 如果起始為0,則產生一個新檔.
            {
                File.Create(FilePath).Close();
            }

            else if (!File.Exists(FilePath))
            {
                CustomSoapException("檔案不存在", String.Format("檔案 : {0} 不存在", FilePath));
            }

            long FileSize = new FileInfo(FilePath).Length;
            if (FileSize < Offset)
            {
                CustomSoapException("檔案Offset錯誤", "Offset大於檔案長度");
            }


            //開啟檔案寫入.
            try
            {
using (FileStream fs = new FileStream(FilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.Read))
                {
                    fs.Seek(Offset, SeekOrigin.Begin);
                    fs.Write(buffer, 0, buffer.Length);
                }

            }

            catch (Exception ex)
            {
                CustomSoapException("檔案寫入錯誤", ex.Message);
            }

    }

    #endregion


    #region Download
    /// <summary>
    /// 下載檔案
    /// </summary>
    /// <param name="FileName">檔案名稱 string</param>
    /// <param name="Offset">檔案起始 long</param>
    /// <param name="BufferSize">傳輸量 long</param>
    /// <returns>檔案 byte[]</returns>
    [WebMethod]

    public byte[] DownloadChunk(string FileName, long Offset, int BufferSize)
    {
            string FilePath = Path.Combine(UploadPath, FileName);

            //檢查檔案是否存在
            if (!File.Exists(FilePath))
                CustomSoapException("檔案不存在", String.Format("檔案 : {0} 不存在", FilePath));

            long FileSize = new FileInfo(FilePath).Length;

            //如果檔案起始位置大於檔案長度
            if (Offset > FileSize)
                CustomSoapException("錯誤的下載位置", String.Format("檔案大小為 {0}, 要求的起始位置為 {1}", FileSize, Offset));


            byte[] TmpBuffer;
            int BytesRead;

            try
            {
                //開啟檔案
                using (FileStream fs = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    fs.Seek(Offset, SeekOrigin.Begin);    // 找到要求的檔案起始位置
                    TmpBuffer = new byte[BufferSize];
                    BytesRead = fs.Read(TmpBuffer, 0, BufferSize);    // 把資料讀入buffer內
                }


                if (BytesRead != BufferSize)
                {
                    //最後一個檔案長度buffer可能會比BufferSize少,所以必需改變buffer的長度,以符合實際檔案長度.
                    byte[] TrimmedBuffer = new byte[BytesRead];

jeffyeh 發表在 痞客邦 留言(0) 人氣()

這幾天在Trouble shooting時,有遇到一個特別的狀況,就是兩台DB的編碼設定不同,一台是設BIG5,另一台是UTF8,UTF8的資料會是查詢條件帶到BIG5 那台去,當遇到稀有字時(例如:鍈),就會有問題,這個字在BIG5顯示出來的會是"?",而UTF8則是正常,那麼從UTF8帶出的"鍈",要怎麼去 BIG5找出來?


這時找到一個方法,就是把UTF8的string轉為BIG5,這樣就能到DB去比對出來了.


Sample Code as Below.


    public static string ConvertUTF8toBIG5(string strInput)
    {
        byte[] strut8 = System.Text.Encoding.Unicode.GetBytes(strInput);
        byte[] strbig5 = System.Text.Encoding.Convert(System.Text.Encoding.Unicode, System.Text.Encoding.Default, strut8);
        return System.Text.Encoding.Default.GetString(strbig5);
    }


之前寫的時候,沒想到這個問題,今天它就用"鍈"這個字來逞罰我,下次寫程式的時候,要注意到編碼的問題.

jeffyeh 發表在 痞客邦 留言(0) 人氣()

  最近因為工作的關係,接觸到了MTOM這個技術的東西,而所要做的就是籍用呼叫Web Service的方式,呼叫端就可以把檔案上傳,而這過程中,如果傳輸中斷,它必需要能夠"斷點重傳",這樣就不用再重頭傳起,而這傳輸速度不能太慢,當時光聽到要用Web Service來傳檔案,想到的就是它怎麼可能會快,不過就在慢慢瞭解了一下MTOM的技術後,似乎可以試看看,可能速度沒有我想像中的那麼慢.



 MTOM是幾年前所冒出來的一個技術,剛接觸到這個名詞時,我是一頭霧水,不知道這又是什麼怪東西,MTOM為Message Transmission Optimization Mechanism的簡寫,中文名稱就是『訊息傳輸最佳化機制』,簡單的來說,這個技術就是以未經處理的位元組形式,以二進位附件與SOAP訊息一起傳輸的方式,因此它的傳輸資料量就會比較少,而且二進位的傳輸方式會快於文字傳輸。


  這個技術除了WCF已經可直接使用外,也可安裝WSE(Web Services Enhancements)來使用,後續所有的討論將是以WSE為主,在開發過程中發現它的中文參考資源很有限,所以把這段時間所學到的東西整理寫出來, 或許這點東西會有點幫助,拉回正題,它所使用的.Net Framework為2.0(含)以上,目前的版本為3.0,我想也不會再有新的版本了,因為後續就是WCF的世界.
 
  WSE這個套件在Visual Studio的add-in支援並不是那麼的全面,像Visual Studio 2005 Express Editions就有問題,VS2008也不支援(整合到WCF了,所以WSE就沒有add-in的必要吧),不過沒有add-in也沒什麼大問題,它還 是有WSE configuration tool可以使用,只是在開發上有點不便,但不致於這個技術不能使用,這個configuration tool主要是用來設定app.config與web.config,後續會介紹到怎麼去使用這個工具來設定.
 
(而VS2005之前的版本例如VS2002及VS2003就”完全”不支援,因為首段就提到,它是在.Net 2.0以上,而2002及2003是使用.Net 1.X,所以是完全不支援)
 
  MTOM就先大至介紹到這,有興趣的人可以在網路上搜尋到更多相關資訊,我這就不再繼續介紹補充,後續將會拿它來玩一個有趣的功能,就是用 它來傳檔案,而且還可以支援斷點續傳的功能,(多點續傳也是OK,但因為寫下去就多了,反正它只是額外的功能延伸,基礎上是一樣的,所以沒寫多點續傳的部 份),使用Web Service來傳檔案,以過去傳統的技術來想像,會是很可怕的慢,可是在MTOM技術上的表現卻是意料之外的好,主要差別是兩者的傳輸資料型態不同,那 跟WCF比較起來呢?從微軟的官方文件看來,WCF可是比WSE快了200%~300%,看起來真是心動的數字啊,不過不知道它的比較方式跟標準為何,未 來有機會再玩,這次先拿WSE來玩檔案傳輸。
 
待續……..
 
參考資料:

下載WES 3.0

jeffyeh 發表在 痞客邦 留言(0) 人氣()

在下SQL Script,大家都知道使用Parameter來傳入參數會比較安全,但在各種不同的DB使用下,Parameter的符號也會不同.


 


使用ADO.Net for Microsoft SQL


符號 = @


EX :


Select * from testtable where id=@id


 


使用ODO.Net for Oracle


符號 = :


EX :


Select * from testtable where id=:id


補充 :


OracleMS SQL有點不同,如果傳入的值是空字串,Oracle要把它轉為DBNull.Value,MS SQL不用.


cmd.Parameter.Add(“id”,DBNull.Value)


不然會跳出這樣的Exception


Parameter 'id': No size set for variable length data type: String


 


使用ODBC / OLEDB


使用符號 = ?


EX :


Select * from testable where id=?


補充 :


這個可以發現到一點,參數名稱只有符號?,後面沒有參數名稱,那怎麼去對應到Parameter?,它是對應到Parameter.Add的先後加入順序,所以跟加入的順序有關.所以在Add,順序與Script裡的順序要相同.


 


以上的Parameter如果加入的數量與Sql Script裡使用的Parameter數量不同時,是會觸發Exception.


EX :


少傳參數時 :


部份變數未被連結


多傳參數時 :


變數名稱?號碼無效


 


參考 :


SqlCommand.Parameters屬性


OracleCommand.Parameters屬性


Null in Oracle


OdbcCommand.Parameters屬性


OleDbCommand.Parameters屬性

jeffyeh 發表在 痞客邦 留言(0) 人氣()

在很多程式裡都有四捨五入Round的功能,但其四捨五入的結果是否真如預期? 負數的四捨五入是什麼結果?以下用JavaScript / .Net / Oracle /MS SQL /Excel  這幾種語言或工具來看看,結果都是出乎預料之外...


 

正數的四捨五入大都多什麼問題,問題在於負數,負數要怎麼四捨五入? 


 

JavaScript


Math.round(-0.51) = -1


Math.round(-0.5) = 0 ç 竟然沒有進位!


Math.round(-0.49)=0


 


Math.round(0.51) = 1


Math.round(0.5) = 1


Math.round(0.49)=0


 


.Net


Math.round(-0.51)=-1


Math.round(-0.5) = 0 ç 竟然沒有進位!


Math.round(-0.49)=0


 


Math.round(0.51) = 1


Math.round(0.5) = 0 ç 負數沒進位就算了,正數也沒有進位!


Math.round(0.49)=0


 


Oracle


Select round(-0.51) from dual =-1


Select round(-0.5) from dual =-1


Select round(-0.49) from dual =0


 


Select round(0.51) from dual =1


Select round(0.5) from dual =1


Select round(0.49) from dual =0


 


MS Sql


Select top 1 round(-1.51,0) from testable =-2


Select top 1 round(-1.5,0) from testable =-2


Select top 1 round(-1.49,0) from testable =-1


 


Select top 1 round(1.51,0) from testable =2


Select top 1 round(1.5,0) from testable =2


Select top 1 round(1.49,0) from testable =1


: 這時有個怪現象,如果用0.X,會出現錯誤訊息.


錯誤訊息為 : 執行批次時發生錯誤。錯誤訊息為: 算術溢位。


因此,測試數據為1.51 / 1.5 / 1.49…çMS SQL拒算0.X的小數字?


 


Excel


Round(-0.51) = -1


Round(-0.5) = -1


Round(-0.49) = 0


 


Round(0.51) = 1


Round(0.5) = 1


Round(0.49) = 0


 


 


  看了以上的清單後,或許有些人會覺得,怎麼.Net的結果這麼的奇怪,跟我們所認知的四捨五入不同,其實它沒有錯哦,這些只是誤會,MSDN裡可以看到一點,它所遵循的是IEEE Standard 7544,這種捨入有時稱為捨入至最接近值。如果要捨入值為偶數,則捨去,如果為奇數,則進位,有看沒有懂哦,用下列幾個數字來解說一下.


 


0.5 è 0


*因為要捨入值為偶數0,所以捨去0.50


1.5 è 2


*因為要捨入值為奇數1,所以進位0.52


2.5 è 2


*因為要捨入值為偶數2,所以捨去0.52


3.5 è 4


*因為要捨入值為奇數3,所以進位0.54


 


這個就是所謂的四捨六入五成雙(Banker’s Rounding)~


 


那要怎麼做才能達到我們原本預期的四捨五入?


.Net 2.0以上的Math.round多了一個MidpointRounding的參數可以傳入.


MidpointRounding.ToEven [Banker’s Rounding]


MidpointRounding.AwayFromZero [這就是大家小學所熟悉的四捨五入]


用法 :


Math.round(0.5,0,MidpointRounding.AwayFromZero) = 1


 


 


參考 :


MSDN Math.Round 方法


Math.Round 方法 (Decimal, MidpointRounding)


 


jeffyeh 發表在 痞客邦 留言(0) 人氣()

或許有些人在使用DB Connection,會有一個疑問,究竟最後是要把ConnectionCloseDispose,還是直接Dispose就好呢?Dispose,Connection也會一同Close?


 


先把CloseDispose這兩個Method給分開來看,Close是把連線給關閉,Connection物件依然存在,所以是可以再Connection.Open來使用的,Dispose,就是把Connection執行個體所使用的Unmanaged資源給釋放,所以要再使用它就必需重新建立New一個執行個體出來.


 


到這裡大家都沒有什麼問題,問題在於呼叫Dispose,是否也會Close Connection? MSDN上所看到的答案是 : 會的!


MSDN 說明 :


If the SqlConnection goes out of scope, it won’t be closed. Therefore, you must explicitly close the connection by calling Close or Dispose. Close and Dispose are functionally equivalent. If the connection pooling value Pooling is set to true of yes, the underlying connection is returned back to the connection pool. On the other hand, if Pooling is set to false or no, the underlying connection to the server is closed.


 


說真的,這幾年寫程式總是習慣先呼叫Close再呼叫Dispose,其實這也沒什麼錯,明確指令是比較好(誰知道會不會那天Framework更版也改了Dispose,就不會順便幫我們Close),當然,Using也是個不錯的用法,離開Using就會自動CloseDispose.


 


MSDN裡的說明,還有一段也是之前寫程式沒注意過的.因為我都是安份的自己RollBack,沒用Close玩過RollBack.


The Close method rolls back any pending transactions. It then releases the connection to the connection pool, or closes the connection if connection pooling is disabled.


也就是說,當呼叫Close,如果還有一些未CommitTransaction,就會被RollBack.


雖然如此,我還是喜歡自己RollBack,避免未來造成閱讀困難.


 


       Connection.Close是否可在已經Close的情況下,再呼叫Close?答案是可以的,但如果是Connection.Open,就不可以在已經Open的情況下再次呼叫Open,這就會觸發Exception.


 


參考 :


MSDN SqlConnection.Close Method

jeffyeh 發表在 痞客邦 留言(0) 人氣()

  在Oracle裡有一個不錯的階層式查詢功能,可以用很簡短的Script來達成目標,階層的意思就有點像是(總經理->副總->協理->經理->副理…..)的這種層級關係,這樣看可能還是很難懂階層或查詢的用法關係,以下是Demo的資料表:


 

TableName : UserMgrInfo












































UserNo

[人員編號]

UserName

[人員名稱]

UserMgrNo

[主管代號]

UserActive

[人員生失效]

Z001

Jeff

X059

1

X059

Kevin

X043

1

X040

Judy

B001

1

X043

David

X040

0

B001

Andy

A001

1

A001

Frank

 

1

C001

Cindy

B001

1

 

  上面這個表格的關係很簡單,每筆資料都有一個主管代號,這個主管代號就是他的上一階主管,當UserMgrNo為空時,代表為最上階主管,階層結束,之中也有人員生失效的控制,現在要做的是如何下一個查詢,找出該員的主管清單?而且只要找出生效的人員。

 

  在Oracle裡有個Start with connect by prior可以使用,以下的語法就是找出該員的生效主管清單:

 

Select UserNo,UserName from

(

        Select UserNo,UserName,UserActive from UserMgrInfo

        Start with UserNo=’Z001’

        Connect by UserNo=prior UserMgrNo

)

Where UserActive=’1’

 

這個語法所下的條件,就是找出編號Z001這個人的主管清單,這個語法所輸出的結果如下 :






















UserNo

[人員編號]

UserName

[人員名稱]

Z001

Jeff

X059

Kevin

X040

Judy

B001

Andy

A001

Frank

  而編號X043的David並不會輸出,因為他是失效人員,為什麼要用子查詢的方式去下UserActive=’1’,而不直接下在裡面就好? 因為如果直接下在裡面,這個查詢結果就只會到X059就停下來了,這個情況就很像rownum :

 

Select * from UserMgrInfo where rownum=1

這時會帶回第一筆資料

Select * from UserMgrInfo where rownum=2

這時並不是帶回二筆資料,而是一筆也沒有,如果要帶回第二筆,就要用子查詢的方式

Select * from

(

Select rownum,* from UserMgrInfo

)

Where rownum=2

 

  回正題,如果此時要查Cindy編號C001的主管清單,同樣的語法所帶回的結果如下 :
















UserNo

[人員編號]

UserName

[人員名稱]

C001

Cindy

B001

Andy

A001

Frank

所以用這個Start wit connect by prior就可以很快的達成階層式查詢囉~ 

 

參考資料 :

jeffyeh 發表在 痞客邦 留言(0) 人氣()


  相信有很多人都知道Try-Catch這個東西,只要是寫在TryCatch裡面的程式發生錯誤,就會跳到Catch的地方,最後就一定會執行finally區段(如果有寫的話).如此方便的做法,使用也相當的普遍,但也常被討論它的效能問題,記得早期曾看過一段程式碼,可能當時工程師不知道如何去判斷使用者輸入的文字是否為數字(C#並沒有IsNumeric,但可以用Double.TryParseInt32.TryParse等的方式去判斷),所以這段程式碼所用的方式是用Try Catch去包一段程式,如果因為非數字而Parse失敗,就跳到Catch,所以這種效能自然差到不行,這個錯誤的例子是依懶TryCatch來做判斷,需記得TryCatch不是拿來這麼用的,只是該程式並沒有辨法使用TryParse或是CharIsNumber,因為執行平台是在是.Net1.0,Double也必需到1.1才支援,後續改用其它方式去判斷,雖然程式碼變多,但效能比用TryCatch觸發Exception來的快很多.


 


TryParse支援版本及參考清單


型別


.Net支援()版本以上


參考


Boolean


2.0


Boolean.TryParse Method


Byte


2.0


Byte.TryParse Method


Char


2.0


Char.TryParse Method


DateTime


2.0


DateTime.TryParse Method


Decimal


2.0


Decimal.TryParse Method


Double


1.1


Double.TryParse Method


SByte


2.0


SByte.TryParse Method


Single


2.0


Single.TryParse Method


IntXX


2.0


Int32.TryParse Method


UIntXX


2.0


UInt32.TryParse Method


備註


Char可以用IsNumber去判斷是否為數字,.Net1.1以上就可以用了.參考MSDN上的說明.


使用TryParse判斷字串是否為數值


*別誤解TryParse是用來判斷數值.它是用來轉型(Parse)並回傳成功或失敗的結果,DateTime來說,TryParse就是判斷字串是否轉換日期格式成功.


 


   


  TryCatch最常見的賴人寫法就是直接抓通用的Exception,例如 :


Try


{


  Code here…..


}


Catch (Exception ex)


{


  MessageBox.Show(ex.Message);


}


  其實Exception有相當的多種類,例如SqlExceptionArgumentNullException,有興趣的人可以參考MSDN上所提供的資訊,依自己所需要的情況去抓取Exception訊息,例如某段對DB操作的程式碼,如果有發生DB的錯誤,需要將錯誤寫,Log,其它Exception就不需要,就可以用SqlException去抓到DB相關的錯誤.


  可是TryCatch因為效能的關係,可別將它一股腦的在程式的每個地方都用TC包起來,如果確定不會出問題的,就不用包了,TC用在一些難以預期的意外上或是刻意要抓取的例外,就很好用,例如DB的連線,沒人可以保證你的程式在與DB連線時,100%不可能斷線,或是IO寫檔到一半,磁碟機斷線或是掛了,這個程式碼很難防,好不容易寫了一堆程式碼來防範,結果下次又因為別的狀況又掛了,此時還是TryCatch好用.


  即然提到它的缺點類別,那它在一些特殊用法時,它會是什麼樣的情況,例如巢式TryCatch,呼叫端與被呼叫端均有TC包起來,如果發生Exception,那會是那個TC被觸發?以下用幾個簡單的例子來說明 :


 


Ex 1 :


try


{


int i = 0;


        int k = 10 / i;


        try


       {


            int a = 0;


            int b = 10 / a;


}


       catch


       {


            MessageBox.Show("inside");


}


}


catch


{


MessageBox.Show("Out Side");


}


 


  紅色字的部份,就是會發生錯誤的地方,當執行到第一個k=10/i,就會發生錯誤,就會跳到Out Sidecatch,inside的部份並不會執行到.


 


Ex 2 :


try


{


    try


    {


        int a = 0;


        int b = 10 / a;


     }


    catch


    {


        MessageBox.Show("inside");


     }


     int i = 0;


     int k = 10 / i;


}


catch


{


     MessageBox.Show("Out Side");


}


  Ex2的做法類似Ex1,不過這次把裡面的TC往上移,這次執行就會執行到第二層的TC,所以也會觸發Exception,但只會觸發insideException,OutSide的並不會被觸發,Out Side的部份則是執行到k=10/i,才會觸發,也就是說,第二層TC觸發後,並不會直接跳到外層的TC,它還是會往下執行.


 


EX3 :


private void button4_Click(object sender, EventArgs e)


{


     try


    {


          Ex2();


     }


    catch


    {


            MessageBox.Show("From click");


     }


}


 


 


private void Ex2()


{


    int i = 0;


    int k = 10 / i;


}


呼叫function的程式寫法也很普遍,但如果今天被呼叫的function發生錯誤,而且沒有包TC,用呼叫端去包function,也是同樣可以catch到錯誤,以這個例子來說,還是可以抓到Message”嘗試以零除。”.


 


EX4 :


private void button7_Click(object sender, EventArgs e)


{


     try


    {


            Ex5();


     }


    catch (Exception ex)


    {


            MessageBox.Show(ex.Message);


     }


}


 


private void Ex5()


{


     try


    {


        int i = 0;


        int k = 10 / i;


     }


    catch (Exception ex)


    {


            throw new Exception("Error : "+ex.Message);


     }


}


  如果有時候希望回丟的訊息除了原本的Message,能再加上額外的訊息供判斷,例如錯誤單號或是檔案,就能更清楚的找到錯誤資料,這時就可以用throw new Exception的方式來丟資訊,這個例子所Show出來的Message就會是”Error : 嘗試以零除。


  Exception可以抓到的不只是目前例外狀況的訊息而已,也可取得造成錯誤的程式或物件名稱等,finally也是很重要的一環,不管有沒有觸發Exception,finally區段一定都會執行到,所以善用TCException所提供的資訊,相信能更容易的掌握問題點,Try-Catch-Finally所包住的程式效能並不會有多大的影響,效能只有在觸發Exception時才會比較差,所以該用的時候還是要用,別把TryCatch當文章開頭提的一樣,用來當判斷用,那效能真的是會出人命,而且Exception是可以抓到所有的Exception沒有錯,但效能會差一點,如果能明確指定是那一種Exception,效能會比較好,例如以下寫法.


try


{


        Code here.


}


catch (SqlException se)


{


        Section 1


}


catch (Exception ex)


{


        Section 2


}


  如果是發生SqlException相關的錯誤,那就會執行Section 1這段,就不會執行Section 2,如果發生的是SqlException外的,那才會去執行Section 2.


那為什麼明確指定Exception種類的效能會比較好呢,因為它就不用再到共同Excption去一個一個找出錯誤的資訊,減少查詢範圍.


 


參考資訊 :


MSDN Exception屬性


MSDN 理和擲回例外狀況


MSDN 結構化例外處理概觀


Visual Basic .Net的效能最佳化

jeffyeh 發表在 痞客邦 留言(0) 人氣()

Sql的語法可以做到Distinct的功能,取出不重覆的資料來,但如果不是從DB取資料,來源可能是DataTable,那要怎麼辨?


之前Google了一下,找到了Topcat分享的技術[ 如何從DataTable中取出Distinct的資料 ],果然簡單又有效率.


tmpDataTable.DefaultView.ToTable(true,"唯一的欄位名");


這麼一行就可以取得tmpDataTable裡不重覆的資料,果然是很簡單的好東西.


但一定也有不少人事後跟我有一樣的反應.....


什麼!!!!只有.Net 2.0以上才有!!! 那麼1.X的怎麼辨??


要做也不是不能做,就跑跑迴圈去做囉,不過最近看到一個有趣的做法,可以應用在某些狀況下. 當然這個在.Net 1.x也可以用.


 


首先在PageLoad的地方塞入一些資料Demo用.


        DataTable dt = new DataTable("TestDT");
        private void Form1_Load(object sender, EventArgs e)
        {
            dt.Columns.Add("Loc");
            for (int i = 0; i < 15; i++)
            {
                DataRow dr = dt.NewRow();
                dr["Loc"] = new Random(i).Next(10);
                dt.Rows.Add(dr);
            }

            dataGridView1.DataSource = dt;
        }

 


再來就加一個Function,這個Function就是這次的主角,傳入兩個參數,一個是存著全部資料的DataTable,另一個就是要Distinct的欄位,回傳的string陣列就是Distinct的清單,這個Sample Code是在MSDN上看到的,不過有個地方不是很好,"老師"有教過,不要拿Try-Catch來做資料正確的判斷,這樣的效能很差,所以呢,就小改了一下,加上了第8行,看有沒有重覆的Key,沒有再Add進去.


01         public string[] GetDistinctValues(DataTable dtable, string colName)
02         {//FYI : http://msdn.microsoft.com/zh-cn/library/system.data.datatable.defaultview(VS.80).aspx
03             Hashtable hTable = new Hashtable();
04             foreach (DataRow drow in dtable.Rows)
05             {
06                 try
07                 {
08                     if (!hTable.ContainsKey(drow[colName]))//判斷是否有重覆的Key
09                     {
10                         hTable.Add(drow[colName], string.Empty);
11                     }

12                 }

13                 catch (Exception ex)//MSDN是用Try-Catch的Error去限制唯一Key,效能會很差,這裡不這麼用,只用來判斷其它錯誤.
14                 {
15                     MessageBox.Show(ex.Message);
16                 }

17             }

18             string[] objArray = new string[hTable.Keys.Count];
19             hTable.Keys.CopyTo(objArray, 0);
20             return objArray;
21         }

接下來就是要呼叫這個Function囉,第一行就是呼叫它,回傳string array到tmp變數內,後續就把它放到DataTable裡,用dataGridView去show出來.


01             string[] tmp = GetDistinctValues(dt, "Loc");
02             DataTable tmpdt = new DataTable("TestDT");//將Distinct的結果放到新的DataTable
03             tmpdt.Columns.Add("Loc");
04             foreach (string s in tmp)
05             {
06                 DataRow dr = tmpdt.NewRow();
07                 dr["Loc"] = s;
08                 tmpdt.Rows.Add(dr);
09             }

10             dataGridView1.DataSource = tmpdt;

這個用法很有趣,沒什麼特別的技術,但卻是一個之前沒想到的簡單邏輯,或許有人會說,有這唯一的Key沒有用啊,沒有其它資訊,換個方式想,可以用DataView.RowFilter或是DataTable.Select,用這Key就可以取得資料了.


.Net 1.x的用法沒有2.X以上的簡潔好用,但也算是不是方法的方法.


 


絫考資料 :


MSDN DataTable.DefaultView屬性


MSDN DataView.RowFilter 屬性


MSDN DataTable.Select 方法

jeffyeh 發表在 痞客邦 留言(0) 人氣()

最近遇到一個有趣的怪問題,同樣的程式碼,在VS2002或3及VS2005或8有不同的執行結果,因為VS2002及VS2003的結果一樣,VS2005與VS2008的一樣,所以後續就以.Net 1.X及.Net2.X以上來分別.




首先,不知道大家在看到這一行Code的時候,心中所想到的答案是什麼.


Convert.ToInt16(0).Equals(0)


答案應該是True or False?




執行結果很有趣,在.Net 1.x的執行結果是False.


然而,在.Net 2.X的執行結果卻是True!!!




為什麼同樣的Code會有不同的結果?


瞭解了一下Object.Equals,其實它所比較的不只是值,還比較型別上是否相同.




Int16的型別就是所謂的short,而我放在Equals裡的0則是Int32,所以.Net 1.x的平台在Convert.ToInt16(0).Equals(0)的情況執行下,雖然值是相同,但因為型別不同,所以就視為不同,這段改為Convert.ToInt32(0).Equals(0),就會得到True.


那為什麼.Net 2.X以上卻會得到不同的結果?




這時要看一下不同的.Net平台,在同樣傳入Equals()裡的值被視為什麼.


.Net 1.X


Int16.Equals(object obj)


ValueType.Equals(object obj)


Object.Equals(object obj)


.Net 2.X


Short.Equals(object obj)


Short.Equals(short obj)




從這裡就可以發現到不同啦,在.net 1.X的平台下,裡面的值都是視為object,所以還會比較object的型別,而.Net 2.X的平台下,因為傳入的值不僅有object,還有short這個型別,所以此時在Equals裡的0就"自然"的視為short的囉.


所以在.Net 2.X在平台下,宣告個變數Int32.


Int32 i=0;


Convert.ToInt16(0).Equals(i);


在這邊被傳入的 i 是視為object,會用其原本的型別來判斷.因為變數的型別不同,這時就會得到False囉,



jeffyeh 發表在 痞客邦 留言(0) 人氣()

  最近因為在思考如何驗證取得的檔案與來源端相同,比對檔案大小是個方法,但不是很好,畢竟它也可能同大小,但內容不一樣,於是就想到 Checksum這個名詞,大概找了一下網路上的資料,發現方法各有不同,所以算出來的結果也會不同,想來想去,反正來源端的checksum的運算方式 只要同目的端,這樣就可以達成驗證的目標了.


  這次用的方法其實也很簡單,不論是MD5或SHA1都不過兩三行的程式即可達成,這次就用VS2008的C#,來用WinForm做Demo,這個功能會用到加密的功能,及讀檔的部份,所以用到以下幾個namespace.


using System.IO;


using System.Security.Cryptography;




SHA Code :


            if (openFileDialog1.ShowDialog() == DialogResult.OK)
            {
                using (FileStream fs = File.OpenRead(openFileDialog1.FileName))
                {
                    SHA256Managed sha = new SHA256Managed();
                    txtResult.Text = Convert.ToBase64String(sha.ComputeHash(fs));
                    txtPath.Text = openFileDialog1.FileName;
                }
            }


MD5 Code :
            if (openFileDialog1.ShowDialog() == DialogResult.OK)
            {
                using (FileStream fs = File.OpenRead(openFileDialog1.FileName))
                {
                    MD5 m = MD5.Create();
                    txtPath.Text = openFileDialog1.FileName;
                    txtResult.Text = Convert.ToBase64String(m.ComputeHash(fs));
                }
            }


   看一下MD5及SHA的程式碼幾乎一樣,但各會依據其加密方式取回checksum,如果檔案內容有所不同,則checksum就會不同,記得看過一篇 討論,如果日期時間相同,就會產生相同的checksum,但從這程式碼中就可以發現,加密的來源是檔案內容,並不是檔案屬性,這也才是所要驗證的目的.




範例下載 : ChecksumTest.rar

jeffyeh 發表在 痞客邦 留言(0) 人氣()

  好久沒寫GPS相關的東西了,前一段時間一直在忙東忙西的,之前有寫兩篇跟GPS相關的文章,主要是在如何從GPS取得目前所在座標,最近在 MSDN Magazine看到一篇如何計算兩點座標的範例,剛好是我前一段時間一直在找的答案,當時卡在的一個問題點是如果兩點是直線,那就很好算了,但地球是圓 的,所以就不是算兩點直線,需使用球面余弦定率,探討的愈深入,我會愈想睡.....所以MSDN Magazine真是佛心來的,這次就有這個範例,我也不用去想破腦子,不過Magazine上的範例並不完整,有個Function沒提供,所以根本不 知道它呼叫這個Function還有做什麼,所以在這補上那個未加入的Function (如果有誤,也請指正).


Magazine上的範例 :


01         public double GetDistance(double Lat1, double Long1, double Lat2, double Long2)
02         {
03             double Lat1r = ConvertDegreeToRadians(Lat1);
04             double Lat2r = ConvertDegreeToRadians(Lat2);
05             double Long1r = ConvertDegreeToRadians(Long1);
06             double Long2r = ConvertDegreeToRadians(Long2);
07
08             double R = 6371; // Earth's radius (km)
09             double d = Math.Acos(Math.Sin(Lat1r) *
10                 Math.Sin(Lat2r) + Math.Cos(Lat1r) *
11                 Math.Cos(Lat2r) *
12                 Math.Cos(Long2r-Long1r)) * R;
13             return d;
14         }

15

補充 Function :


1         private double ConvertDegreeToRadians(double degrees)
2         {
3             return (Math.PI/180)*degrees;
4         }



  之前一直想做這個,因為可以做出像"龍珠雷達"的東西(太迷七龍珠了...),用途咧,可以用來玩尋寶遊戲,還有我每年都會痛一次的掃墓,因為很多墓地不是放在靈骨塔,都是在很偏僻的地方,每年都花很多時間在找墓地,如果用Papago之類的導航程式都會亂跳(顯靈了嗎?),所以直接儲存墓地的座標,下次再找的時候,就可以知道目前位置與目的點的距離.




參考資料 :


MSDN Magazine : 使用Mappoint和GPS的位置感知應用程式

jeffyeh 發表在 痞客邦 留言(0) 人氣()

  寫Master及Detail連動的功能很常見,就是點擊主清單時,在副清單會顯示出該項目的明細資料,但很多時候因為資料量的考量,都是點擊 主清單時,再依點擊的項目至資料庫取回明細資料,這種寫法用久了,很容易就忘了還有DataRelation這種東西,沒錯,如果一開始就將所有的明細資 料都從資料庫取出,資料量是很可怕的,所以點主檔再取明細是較好的做法,那DataRelation不就沒用處了?答案不盡然,就看是否用在對的地方,那 些方面可以運作就不介紹了,接下來要講的是如何使用DataRelation.


這範例所表現出來的畫面如下 :


程式開始會先產生一些基本資料,左邊的Grid為主檔,右邊則為相關明細,當我點左邊的Judy時,右邊就會顯示Judy的資料,注意看,No是一樣的.



當我點擊Jack後,右邊的明細馬上就會自動帶出Jack的資料,此時的No一樣都是1了.



  要做到這樣的效果一點也不難,也不用很久,除去基本資料的準備,建立DataRelation及丟到Grid,不過就五行程式碼(也可說只有三行,如果丟到DataGrid.DataSource不算).


程式碼如下 :


01         private void Form1_Load(object sender, EventArgs e)
02         {
03             try
04             {
05                 #region 準備預設顯示資料
06                 DataSet ds = new DataSet();
07                 DataTable dtm = new DataTable("BasicMaster");
08                 dtm.Columns.Add("UserNo", typeof(int)).ReadOnly = true; ;
09                 dtm.Columns.Add("UserName", typeof(string));
10                 dtm.Columns["UserNo"].AutoIncrement = true;
11
12                 DataRow dr1 = dtm.NewRow();
13                 dr1["UserNo"] = 0;
14                 dr1["UserName"] = "Judy";
15                 dtm.Rows.Add(dr1);
16
17                 DataRow dr2 = dtm.NewRow();
18                 dr2["UserNo"] = 1;
19                 dr2["UserName"] = "Jack";
20                 dtm.Rows.Add(dr2);
21
22                 DataTable dtd = new DataTable("BasicDetail");
23                 dtd.Columns.Add("UserNo", typeof(int)).ReadOnly = true;
24                 dtd.Columns.Add("Phone", typeof(string));
25
26                 DataRow dr3 = dtd.NewRow();
27                 dr3["UserNo"] = 0;
28                 dr3["Phone"] = "123456789";
29                 dtd.Rows.Add(dr3);
30                 DataRow dr31 = dtd.NewRow();
31                 dr31["UserNo"] = 0;
32                 dr31["Phone"] = "987654321";
33                 dtd.Rows.Add(dr31);
34
35                 DataRow dr4 = dtd.NewRow();
36                 dr4["UserNo"] = 1;
37                 dr4["Phone"] = "159753456";
38                 dtd.Rows.Add(dr4);
39                 DataRow dr41 = dtd.NewRow();
40                 dr41["UserNo"] = 1;
41                 dr41["Phone"] = "951357654";
42                 dtd.Rows.Add(dr41);
43                 DataRow dr42 = dtd.NewRow();
44                 dr42["UserNo"] = 1;
45                 dr42["Phone"] = "987456321";
46                 dtd.Rows.Add(dr42);
47
48                 ds.Tables.Add(dtm);
49                 ds.Tables.Add(dtd);
50                 #endregion

51
52                 //建立Relation
53                 ds.Relations.Add(new DataRelation("UserInfo", ds.Tables["BasicMaster"].Columns["UserNo"], ds.Tables["BasicDetail"].Columns["UserNo"]));
54                 BindingSource bsm = new BindingSource(ds, "BasicMaster");//DataSource為DataSet,DataMember為主表單的TableName
55                 BindingSource bsd = new BindingSource(bsm, "UserInfo");//DataSource為主選單的BindingSource,DataMember為RelationName.
56
57                 gridM.DataSource = bsm;//主表單的BindingSource
58                 gridS.DataSource = bsd;//次表單的BindingSource
59             }

60             catch (Exception ex)
61             {
62                 MessageBox.Show(ex.Message);
63             }

64         }

  從第5行到50行都是準備資料,不是重點,重點在第53行,這行就是在建立主檔跟明細檔之間的關係"Relation",兩個Table要用那個欄位來串起來,以這個範來說,UserNo就是兩個Table的關聯欄位.


  接下來第54行跟第55行分別是主檔跟副檔的BindingSource,基本上看起來差不多,但還是有些許差異,在第54行的主檔部份,可以看到new BindingSource(ds,"BasicMaster"),括號內的第一個參數是DataSet,第 二個為主檔的Table名稱,而第55行的副檔部份就不一樣了,第一個參數放的是主檔的BindingSource,第二個參數則為 DataRelation的名稱,接下來只要把這兩個BindingSource分別丟到主副DataGrid的DataSource即可.




就這麼簡單,Master跟Detail的連動就做好了,接下來就看各位怎麼去運用DataRelation的這個功能囉.




參考資料 :


MSDN DataRelation類別


範例下載 :


MDForm.rar


jeffyeh 發表在 痞客邦 留言(0) 人氣()

  今天一早就收到系統寄來的錯誤訊息通知,一看之下,那不是我上週才上線的嗎?而且錯誤訊息是PK重覆,怎麼可能會發生這樣嚴重的程式撰寫錯誤,第一個直覺就是查看資料庫裡的資料,這程式會同時連往Oracle及MS SQL2005兩種不同的資料庫,進行資料的維護更新,看啊看到,發現資料庫裡的資料"不乾淨",資料的後面跟了空白字串,例如 : "TEST  ",正常來說,資料庫裡的資料應該都是Trim過才放進去的,怎麼會有這樣的資料存在.


  這資料是問題之一,但最後發現一個更怪的情況,同一樣SQL語法,Oracle與MS SQL會有不同的結果,但我個人覺得,Oracle才是合理的結果,MS SQL(SQL 2005)是不對的,以Oracle來說 'TEST' <> 'TEST   ',但MS SQL就神奇了,'TEST' = 'TEST  ',它自動幫我Trim,這法範例如下 :


EX1:


select * from testTB where UsrName='TEST'


這個Oracle與MS SQL回傳的結果一樣


EX2:


select * from testTB where UsrName='TEST  '


Oracle會因為TEST後面多了空白字串,而找不到資料.


MS SQL因為它會自動Trim,結果會同EX1.


 


因為這樣的結果,才導致我PK重覆的錯誤.本來很想再查下去,可是沒權限去碰資料庫,只好放棄,只能說,學到經驗了,下次對MS SQL要注意這種問題.

jeffyeh 發表在 痞客邦 留言(2) 人氣()

Null跟DBNull及String.Empty這三者,或許很少有人認真的去想過這三者的差異在那,而什麼時候要用那一個去判斷?


從簡單的if去比對這三者是否相等,很快的就發現這三者是不相等的.


null != DBNull  /DBNull != string.Empty / null != string.Empty


那麼現在宣告了兩個變數,一個是string a="",另一個是string b=null; 那麼這兩個變數是各自符合那一種?


結果是


a != null;


a != DBNull


a = string.Empty


拿.Net 2.0才有的新判斷string.IsNullOrEmpty來看.也是為true


而b的部份則如下


b = null


b != dbnull


b != string.Empty


string.IsNullOrEmpty(b) = true


從這兩個例子看到什麼時候是Null,什麼時候是string.Empty,那麼DBNull呢?


DBNull的使用時機,當至資料庫查詢,符合的資料回傳的資料欄位沒有值,為null時,此時此欄位的值就=DBNull.

 

當至資料庫查詢,沒有符合的資料回傳時,此時回傳的結果就=Null.

 





Null是當物件未被初始化時的情況,例如 StreamReader reader;還沒有給new StreamReader();

 

ex :

object o=cmd.ExecuteScalar();(回傳一個欄位的值)

狀況1:當DB回傳有符合的資料,但該欄位沒有值時.o = DBNull 可是 o != null

狀況2:當DB回傳沒有符合的資料時, o = null可是o != DBNull

狀況3:不論是狀況1或狀況2的結果,Convert.ToString(o)=string.Empty;

 參考 :


MSDN String.IsNullOrEmpty 方法


雖然看起來String.IsNullOrEmpty不錯用,但卻有Bug.


DANGER ! String.IsNullOrEmpty can lead to runtime Null exceptions !!

jeffyeh 發表在 痞客邦 留言(0) 人氣()


  這個的標題有點下,很難用標題點出要分享的內容,這個經驗相信有非常多人都會遇到,只是各自的解法不同,這次所以提的是當資料庫內所儲存的資料是一組多筆記錄組成的字串,例如:


 


TableName : UserMenuRight


資料型態


欄位名稱


欄位說明


Int PK


UserID


使用者代號


Nvarchar


UserRight


使用者權限


 


資料內容


UserID


UserRight


1


A01,A011,A02,A03,A021


2


A011, A03


3


A02,A021,A01


 


 


當我們要找UserRight這欄位的內容含有A01的資料時,SQL語法要怎麼下,才能找到A01,又不會帶出A011,而且A01也可能是字串的最前面,也可能是最後面,也可能在中間,那要怎麼去下條件?


 


  我這是有個做法可以很簡單的去解決這類型的問題,直接在Script裡解決即可,以這個問題來說,語法可以這麼下:


Oracle :


Select * from UserMenuRight where ','|| UserRight ||',' like '%,A01,%'


Sql


Select * from UserMenuRight where ','+ UserRight +',' like '%,A01,%'


 


眼尖的人應該已經看出來了,用此語法,因為資料庫的資料已先前後加逗號,所以就會是A01 , A01,A01,A02,A011,A03,A021, A01,A02,A011,A03,A021, 當我們的查詢條件下like ‘%,A01,%’,不管它在前//後或單一,都可以找到.所以查詢結果就會找到第1,3筆的資料.,第二筆就不會帶出來了.


 


或許這不是最好的方法,但這也是一種解法,如果有其它更好的方式,歡迎分享~



jeffyeh 發表在 痞客邦 留言(0) 人氣()


  或許有些人會因為功能上的需求,所以在一些事件(例如TextChanged)的處理上是使用手動新增,但有時一個不小心,就可能會造成事件的重覆觸發多次,或者當我們在某些狀況下,其實事件是不用觸發的,減少不必要的動作,這時就要把它給停止.


  首先瞭解到一個問題,一般我們的事件都是放在InitializeComponent()裡面,textBoxTextChanged事件為例,在裡面就可以看到this.textBox3.TextChanged += new System.EventHandler(this.textBox3_TextChanged),可是有些狀況我們會是後期才會加入,並不會放在InitializeComponent()裡面,這時就要注意到一個狀況,如果重覆執行this.textBox3.TextChanged += new System.EventHandler(this.textBox3_TextChanged) ,就會造成同一事件多次觸發的情況.


 


舉例來說 :





private void Form1_Load(object sender, EventArgs e)

{

    this.textBox3.TextChanged += new System.EventHandler(this.textBox3_TextChanged);

    this.textBox3.TextChanged += new System.EventHandler(this.textBox3_TextChanged);

}




private void textBox3_TextChanged(object sender, EventArgs e)

{

    MessageBox.Show(“Hello”);

}

 


  我在Form Load的事件中,故意加了兩次TextChanged事件,當我執行去修改TextBox的值時,畫面就會跳了兩次HelloMessageBox出來,也就是說,我加了幾次,它就會觸發幾次,其實會發現這個問題,也是之前遇到的一個經驗,使用者回報說,有隻作業很奇怪,操作愈久,它的速度愈慢,當下只有覺得奇怪,沒想到程式碼裡的問題,但後來去看了一下內容才發現,有個function會新增TextChanged事件,重點是它不只被呼叫一次,所以使用者操作愈多次,這個TextChanged的事件就會觸發愈多,當下就加了一行Code把這個事件給移除.


  即然提到事件的移除,接下來要提的就是如何把事件給移除掉,其實反應快的人可能已經發現了,如果+=是新增事件,那麼-=就會是移除事件,答案沒錯,就是這樣移除, this.textBox3.TextChanged -= new System.EventHandler(this.textBox3_TextChanged),但或許會有個疑問,之前我可能加了三次事件,那麼我是否也要移三次才能移完?答案是不用的,只要移一次,就會把之前的全移除.


 


比如說 :



private void Form1_Load(object sender, EventArgs e)

{

    this.textBox3.TextChanged += new System.EventHandler(this.textBox3_TextChanged);

    this.textBox3.TextChanged += new System.EventHandler(this.textBox3_TextChanged);

}




private void textBox3_TextChanged(object sender, EventArgs e)

{

    MessageBox.Show(“Hello”);

    this.textBox3.TextChanged -= new System.EventHandler(this.textBox3_TextChanged);

}

  第一次修改TextBox,還會觸發兩次MessageBox出來,但當我再去修改TextBox的值時,MessageBox就不會再跳出來了.


 


  像這種技巧我會運用在某些情況,比如說畫面上有兩個TextBox控制項,當使用者在其中一欄輸入代號,就會回資料庫查詢取回此代號的名稱,並顯示於另一個TextBox,這是很常見的一種運用,但如果是使用者使用查詢功能,一次取回此表單上的所有資訊,也就是代號之名稱時,並不需要再額外的去觸發事件回資料庫取值,造成不必要的Request\Response,增加資料庫及網路與系統的負擔,所以這時就可以先把事件給移除”,等畫面更新完,再把事件給新增回來即可.


 


  之前有個表單上有數十個TextBox有這種情況,當輸入值時,會觸發事件,回資料庫取資料,由於輸入時是一格一格輸入,就沒什麼感覺,但如果只是單純的載入這張表單,就會跑很久,其實一開始就把所有的資訊取回了,只是各事件在資料異動時又觸發,又回資料庫取了一次資料,才會導致這個表單的效能不好.後來就在這事件的處理上做了調整,效能就大為的提升.


 


參考 :


MSDN 事件範例


HOW TO:訂閱及取消訂閱事件 (C# 程式設計手冊)



jeffyeh 發表在 痞客邦 留言(0) 人氣()

開始在學習的時候,就知道不可以直接拿UI上的控制項來做運算的動作,例如:



for (int i=0;i<100;i++)
{
    textBox1.Text+=i;
}

  這樣跑下來的效能就差的可怕,因為每更新一次值,就會重繪控制項,而且還會造成畫面的閃動,所以就以上面這個例子,應該先把值存到一個變數內,等運算完成,再把最後結果的變數存至textBox1去.

  但今天如果控制項必需一個一個加進去時,要怎麼辦?例如listbox,是有AddRange可以使用,可是有些狀況必需要用Add,不能用AddRange的方式,這時就要用控制項的BeginUpdate/EndUpdate的方式來處理,基本上AddRange與用Add加上BeginUpdate做法的效能異不大,但與用Add而未加BeginUpdate的方式比較起來,差異就很大了,以下列的程式碼來比較出它們的效能差異.

 

方式1 : 用Add的方式,不使用BeginUpdate



System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
sw.Start();
for (int i = 0; i < 10000; i++)
{
    listBox1.Items.Add(i);
}

sw.Stop();
MessageBox.Show(string.Format("耗時(毫秒) : {0}", sw.ElapsedMilliseconds.ToString("#,##0")));
 

方式2: 用Add的方式,使用BeginUpdate



System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
sw.Start();
listBox1.BeginUpdate();
for (int i = 0; i < 10000; i++)
{
    listBox1.Items.Add(i);
}

listBox1.EndUpdate();
sw.Stop();
MessageBox.Show(string.Format("耗時(毫秒) : {0}", sw.ElapsedMilliseconds.ToString("#,##0")));
 

方式3: 用AddRange的方式



System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
List<object> lt = new List<object>();
sw.Start();
for (int i = 0; i < 10000; i++)
{
    lt.Add(i);
}

listBox1.Items.AddRange(lt.ToArray());
sw.Stop();
lt.Clear();
MessageBox.Show(string.Format("耗時(毫秒) : {0}", sw.ElapsedMilliseconds.ToString("#,##0")));
 

各自執行後的結果如下:

方式1: 950毫秒

方式2: 305毫秒

方式3: 307毫秒

 

  2跟3的差異很小,但都跟方式1差了3倍以上,或許有些朋友的想法是我又不能可會加上那麼多筆資料到listbox裡去,或是才那麼0點幾秒,沒差啦,但有些使用者的硬體設備沒有那麼好,從他們的環境比較起來,可能就不是0點幾秒了,而且系統後續的運用也不見得能如當初所預期的方式去用,小弟就有個例子,當初有個作業在規劃時,評估這個Treeview不會有太多節點,最多幾百個,但後來公司的經營策略調整,這個Treeview瞬間爆增到數萬個節點,硬體設備差一點的,可以中午去吃個午餐,睡個午覺,下午應該就可以用了.

  所以在做此類動作時,除了ListBox,其它如ComboBox,TreeView等,要記得使用BeginUpdate及EndUpdate,避免控制項過度頻繁的重繪,除了造成畫面的閃動外,也增加了系統的負擔.

 

MSDN 說明 :

  將多重項目加入至 ListBox 的慣用方法是使用 ListBox.ObjectCollection 類別的 AddRange 方法 (透過 ListBox 的 Items 屬性)。這可讓您在單一作業中將項目陣列加入至清單中。但如果您要使用 ListBox.ObjectCollection 類別的 Add 方法一次加入一個項目,可以使用 BeginUpdate 方法在每次項目加入至清單時,防止控制項重繪 ListBox。當您完成將項目加入至清單的工作後,請呼叫 EndUpdate 方法以便使 ListBox 進行重繪。這種加入項目的方法可防止當大量項目加入至清單時,出現 ListBox 閃動繪製。

 

jeffyeh 發表在 痞客邦 留言(0) 人氣()

  長久以來,一直有個模糊且不正確的觀念,雖然知道 StringBuilder效能比String好,但並不清楚是在什麼樣的情況下比較好,加上最近有同事提到這部份的效能問題,於是就寫了一些程式來測看 看,而這次反組譯程式也拿出來用了,主要是看原本程式的寫法在經過Compiler後會變為什麼樣子.


 
   這次的測法所用的計時方式,就會用到System.Diagnostics.Stopwatch來計時,所會測的次數除了額外註明的之外,其它均為1萬 次,因為有些跑太多次,我會等到睡著.CPU等級為Intel Dual E2180 2.00 GHz,RAM 2G,由於是String與StringBuilder的比較,所以務求比較寫法的執行結果必需相同,而測試的幾個寫法也是平時較為常見的.
 

  首先所要提到的幾個方式,是字串在設計階段就已經固定,在執行階段也不會有任何改變的做法,這種情況之下,String跟StringBuilder的效能表現,是否StringBuilder還是比String的做法還來的好.


 
String 固定字串組合做法1 (十萬次) :
字串是用+的方式,把每一個String給串接在一起.
public string GetStaticStr1()
{
    string a = "abcdefghijklmnopqrstuvwxyz : " +
           "abcdefghijklmnopqrstuvwxyz : " +
           "abcdefghijklmnopqrstuvwxyz";
    return a;
}
這種做法在Compiler後,就有很明顯的不同.
Public string GetStaticStr1()
{
    Return "abcdefghijklmnopqrstuvwxyz : abcdefghijklmnopqrstuvwxyz : abcdefghijklmnopqrstuvwxy";
}
連變數a也沒有,直接return已組好的字串,所以在執行時,就直接使用組好的字串,沒有再花費其它動作,也因此在效能上表現最好,耗時2毫秒.
 
String 固定字串組合做法2 (十萬次) :
另一種字串的串法也是很常用的做法,使用+=的方式來串接.
public string GetStaticStr2()
{
    string a = "abcdefghijklmnopqrstuvwxyz : ";
    a += "abcdefghijklmnopqrstuvwxyz : ";
    a += "abcdefghijklmnopqrstuvwxyz";
    return a;
}
這種方式在Compiler後,原本以為會跟前一個做法一樣,但結果是有差異的.
public string GetStaticStr2()
{
    string a="abcdefghijklmnopqrstuvwxyz : ";
    return (a+" abcdefghijklmnopqrstuvwxyz : "+" abcdefghijklmnopqrstuvwxyz");
}
因為還有一些變數宣告及串接的動作,所以效能的表現較第一個做法差了一點,耗時32毫秒.
 
String 固定字串組合做法3 (十萬次) :
再來的做法是使用StringBuilder的方式把字串給串起來
public string GetStaticStrBu()
{
    StringBuilder s = new StringBuilder();
    s.Append("abcdefghijklmnopqrstuvwxyz : ");
    s.Append("abcdefghijklmnopqrstuvwxyz : ");
    s.Append("abcdefghijklmnopqrstuvwxyz");
    return s.ToString();
}
 
所以當字串是已知的固定不變文字時,用String來組字串會比StringBuilder還快.
做法1 : 耗時2毫秒
做法2 : 耗時32毫秒
做法3 : 耗時60毫秒
 
  接下來,字串可能會有經過if的判斷式或for迴圈及string.Format的方式去串的做法,其Stringbuilder的效能表現是否就能發揮出來.
String 動態少量字串做法 1 (十萬次) :
  這段字串在組合之前,會先經過if判斷,所以就有可能發生要組或不用組的情況,不過在這個例子下,所做的測試是都要組的,而字串的串接是使用+=的方式.
        int z=0;x=0;
        public string GetDynStr()
        {
            string a = "abcdefghijklmnopqrstuvwxyz : ";
            if (z == x)
            {
                a += "abcdefghijklmnopqrstuvwxyz";
            }

            return a;
        }
 
Compiler後,程式碼並沒有改變,而耗時為14毫秒.
 
String 動態少量字串做法 2 (十萬次) :
測試寫法跟做法1一樣,只不過是使用StringBuilder把它串起來
public string GetDynStrBu()
{
    StringBuilder s = new StringBuilder();
    s.Append("abcdefghijklmnopqrstuvwxyz : ");
    if (z == x)
    {
        s.Append("abcdefghijklmnopqrstuvwxyz : ");
    }

    return s.ToString();
}
Compiler後的程式碼也是一樣,耗時37毫秒.怎麼動態字串的Stringbuilder的表現一樣不如String?
 
   在這個地方先做個小結論,因為這個Case有一個特別的地方,應該是說,我的if的判斷不多,所以字串在串接的次數很少,僅會串接一次,所以如果只有少 量的串接,string與Stringbuilder的效能差異並看不太出來,而String的效能可能還優於Stringbuilder,但當把同樣的 if判斷增加為6個時,其測試結果就不同了,Stringbuilder(121毫秒)的效能就高於string(144毫秒),增加愈多,差異愈大.
 
 

補充 :


  或許用迴圈的方式,容易讓人有種錯覺,感覺自己的程式怎麼可能跑迴圈那麼多次,那就用String就好了,其實應該是要看變數裡的資料量”,只是範例裡用的資料量都不多,所以才用大量迴圈的方式去凸顯出它的效能差異.如果今天資料量很大,不需要用迴圈,一次就可以看出它的差異,就以上面兩個程式做一些修改調整.


 


String 動態大量字串做法 1 (1) :



private void btn_DynStrOnce_Click(object sender, EventArgs e)
{
    System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
    string a = new string('a', 1000000);
    sw.Start();
    DynStrOnce(a);
    sw.Stop();
    MessageBox.Show(string.Format("耗時(毫秒) : {0} ", sw.ElapsedMilliseconds.ToString("#,##0")));
}


public string DynStrOnce(string a)
{          
    if (z == x)
    {
        a += "abcdefghijklmnopqrstuvwxyz";
    }

    if (z == x)
    {
        a += "abcdefghijklmnopqrstuvwxyz";
    }

    if (z == x)
    {
        a += "abcdefghijklmnopqrstuvwxyz";
    }

    return a;
}

 


String 動態大量字串做法 2 (1) :



private void btn_DynStrBuOnce_Click(object sender, EventArgs e)
{
    System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
    string a = new string('a', 1000000);
    sw.Start();
    DynStrBuOnce(a);
    sw.Stop();
    MessageBox.Show(string.Format("耗時(毫秒) : {0} ", sw.ElapsedMilliseconds.ToString("#,##0")));
}


public string DynStrBuOnce(string a)
{
    StringBuilder s = new StringBuilder(a);
    if (z == x)
    {
        s.Append("abcdefghijklmnopqrstuvwxyz");
    }

    if (z == x)
    {
        s.Append("abcdefghijklmnopqrstuvwxyz");
    }

    if (z == x)
    {
        s.Append("abcdefghijklmnopqrstuvwxyz");
    }

    return s.ToString();
}


  此時String的做法耗時9毫秒,Stringbuilder3毫秒,所以當資料量愈大時,String在組字串的效能愈差,Stringbuiler的效能會比String好更多.
 
 
  再下來,所要測的是Format的串接方式,這種做法也有很多人在用,有些是為了程式閱讀性較好,所以就來看看,String與Stringbuilder在用Format的大量串接字串時,各個的表現如何.
 
String 動態大量字串使用Format的做法 1 :
這種做法也很常見,因為程式碼很簡潔,直接把Format後的值放到變數內,同樣達成字串串接的目的.
string tmps = "";
for (int i = 0; i < 10000; i++)
{
    tmps = string.Format("{0}{1}{2}",tmps, "T", i);
}
這做法看來簡潔,但跑出來的結果嚇人,竟然用了1758毫秒.
 
String 動態大量字串使用Format的做法 2 :
這次改一點地方,使用+=的方式來串.
string tmps = "";
for (int i = 0; i < 10000; i++)
{
    tmps += string.Format("{0}{1}", "T", i);
}
跑出來的結果是好多了,但仍然很差,用了687毫秒.
 
String 動態大量字串使用Format的做法 3 :
那麼把這些改為Stringbuilder的方式來串看看.
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
    sb.AppendFormat("{0}{1}","T", i);
}
看起來差異是不大,但效能卻是相當的好,差了幾百倍以上,只耗用了6毫秒.
 
結論 : 果然Stringbuilder在用Format組大量字串時,效能是最好的.
 
  最後要比較的是最簡單的大量字串串接,不使用Format的方式,string直接用+,而Stringbuilder則是用Append的方式來串.
 
String 動態大量字串做法 1 :
string tmps = "";
for (int i = 0; i < 10000; i++)
{
    tmps = tmps+ "T"+i;
}
結果如預期的差,但至少比Format的效能還來的好,耗時680毫秒.
 
String 動態大量字串做法 2 :
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
    sb.Append("T");
    sb.Append(i);
}
  這做法就是最快的,耗時3毫秒,跟String差了2百多倍.所以如非必要性,就不要用Format的方式來串字串,單純的Append的效能反而更好.而大量動態字串在串接時,也千萬別用string,效能真的很差.
 
 
 
  以上的所有寫法,就大概示範出在那種情況下,要用那種寫法,當字串在設計階段時就固定,那麼就用string,用+的方式來串接的效能最好,因為在Compiler後,就會將字串直接組合起來,不需要在run time再組一次,如果是在少量且資料量不大的動態字串串接時,string跟stringbuilder的差異並不大,甚至string可能還比Stringbuilder的表現還來的好,但超過一定數量後或當字串數量很大時,string 就每況愈下,而Stringbuilder就漸入佳境,而Format的方式,效能並不會比直接Append來的好,所以非必要就用Append即可,當 然,最後在大量動態字串串接變現最好的就是Stringbuilder.Append了,主要是因為StringBuilder會在一開始先配置較大的記 憶體位置(可以自行指定Capacity,如果沒指定就採預設16),供作字串相組 之用,如果不夠,才會再重新配置一塊,而String在一開始並沒有預留相組字串用的記憶體空間,所以在組字串時,每次均需重新配置記憶體位置,所以在動 態組字串例如用for迴圈在組時,StringBuilder的表現會優於String. 那麼如果是SQL語法內的變數Parameters呢?例如"select * from TestTable where testid=@tid",以這個例子來說,由於變數是資料庫端在轉換使用的,並不是在程式端就把變數先轉好串接傳進去的,所以這個SQL語法的字串是固定的,並不會因為傳入的變數不同而異動到字串,也因此用string的方式就好.
 
註 : MSDN 效能解說如下.
 
 
Concat 和 AppendFormat 方法都會將新的資料串連到現有的 String 或 StringBuilder 物件。String 物件串連作業永遠都會從現有的字串和新資料建立新的物件。StringBuilder 物件會維護一個緩衝區,以容納新資料的串連。如果有可用的空間,新的資料會附加至緩衝區的尾端,否則,會配置較大的新緩衝區,而原始緩衝區的資料會複製到 新的緩衝區,然後新的資料會附加至新的緩衝區。
 

String 或 StringBuilder 物件之串連作業的效能是根據記憶體的配置頻率而定。String 串連作業永遠都會配置記憶體,而 StringBuilder 串連作業只有在 StringBuilder 物件緩衝區太小而無法容納新資料時,才會配置記憶體。因此,如果要串連固定數目的 String 物件,最好使用 String 類別的串連作業。在這種情況下,編譯器 (Compiler) 甚至可能將個別的串連作業結合成一個單一作業。如果要串連任意數目的字串 (例如,如果迴圈串連任意數目的使用者輸入字串),則對於串連作業來說最好使用 StringBuilder 物件。 


 
參考資料 :
書籍 : Run!PC 2008 2月刊

jeffyeh 發表在 痞客邦 留言(0) 人氣()

1 23