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

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

很多人應該都經歷過一段T-SQL的學習成長路程,最多人最常用的就是直接把使用者輸入的值,直接組到查詢字串裡.


例如 :


string strCmd="select * from testdb where testid ='"+TextBox1.Text.Trim()+";";


很多範例及書藉都有這樣的寫法,可是這寫法有很嚴重的SQL Injection的問題,這也不是個新問題,這是很久以前就有發現的,有些人還以為Hot Fix或Service Pack可以修正SQL Injection的問題,但這是程式寫法的問題,跟Hot Fix或Service Pack沒有關係.


記得過去有位同事,有多年的程式撰寫經驗,後來跟我一起Work了一段時間,他的寫法也是如此,當時提出這個安全問題時,還不被重視,但小弟手就是賤,為了讓上頭重視這個問題,只好自己動手去"注射"了,我沒做什麼,就很單純的關掉DB而已,後來上頭才發現這個問題的嚴重性,才要求更改.


 


後來陸續對安全性的重視提高了,開始想著要怎麼去解決這個問題,就延伸出第二種寫法,開始去過濾關鍵字,置換等消極的方式,有時還會發現新的問題,所以就一直修不停.


例如 :


string strCmd="select * from testdb where testid ='"+TextBox1.Text.Trim().Replace("'","%")+";";


這只是示意性的換掉單引號,但還有很多關鍵字或符號.


 


後來決定要想辦法把使用者的查詢值放到Parameters的方式去處理,而做法也沒想像中的難.


以like的問題來說:


SqlCommand cmd=new SqlComand(); //其它怎麼用Connection等,因為不是重點,所以帶過不寫


cmd.Parameters.Add("@testid",TextBox1.Text.Trim());


cmd.CommandText="select * from testdb where testid like @testid+'%'";


這樣就能做到 like TextBox1.Text.Trim()% 的效果,而且不用擔心使用者輸入的字有什麼關鍵字在裡面.


 


那麼in要怎麼做?


會用in的時候,一定是有很多查詢的值要查,所以做法有很多種,但意思差不多.


例如我的查詢條件有"Jeff","Jack","Jay","Jason",將這些條件個個分開放到一個String Arrary裡.


SqlCommand cmd=new SqlComand(); //其它怎麼用Connection等,因為不是重點,所以帶過不寫


cmd.CommandText="select * from testdb where testid in (";


string strName="Jeff,Jack,Jay,Jason";//要用in查詢的資料


string[] strQry=strName.Split(",".ToCharArray());//把它切到String Array裡去


int intCount=0;


foreach (string sq in strQry)


{


cmd.Parameters.Add("@TestID"+intCount, sq) //所以每次的變數都會不同


cmd.CommandText+="@TestID"+intCount+",";


intCount++;//變數加1


}


cmd.CommandText=cmd.CommandText.Remove(cmd.CommandText.Length-1,1)+")"; //把最後一個逗號換為右括號.


最後就會組出select * from testdb where testid in (@TestID0,@TestID1,@TestID2,@TestID3)


 


未來會不會有第四種改進寫法,就不知道了,看各位有沒有什麼好看法,或是未來會不會我又有遇到什麼問題可以動動腦.


 


#2008/9/13 補充


在網友提供的資訊,發現用Parameters,在用Like時,使用者輸入的%也是當萬用字元在使用.例如:


cmd.Parameters.Add("@testid","Jeff%");


cmd.CommandText="select * from testdb where testid like @testid";


查詢結果會將所有的Jeff開頭的資料取回,而底線 _ 的單一字的萬用字元也是有同樣的效果,那這樣使用者真的想要找資料內有%或是_時的資料時,要怎麼辨才能達成?目前是想到一個方法,但要用到escape這個方式.


例如 :


cmd.Parameters.Add("@testid","Jeff%".Relpace("!","!!").Relpace("%","!%").Relpace("_","!_")+"%");


cmd.CommandText="select * from testdb where testid like @testid escape '!'";


前三個Relpace的動作,是把使用者輸入的! (因為escape 符號是用!),%及_都在前面加個!,讓它被視為原意做為查詢條件,最後加上的%,因為沒前面先加!,所以最後一個%仍為萬用字元使用,所以舉例資料庫的資料如下:


Jeff1


Jeff%1


Jeff%2


Jeff%_1


Jeff2


查詢結果會是 :


Jeff%1


Jeff%2


Jeff%_1


這是目前想到的做法~

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

  用程式去調整EXCEL檔的格式,例如自動對齊,欄位大小,字型顏色等.這個功能平時寫程式很少用到,但有時要用,還真的不是很好找,所以把它給整理一下放上來,說不定那天用到時,就不用再去東翻西找,或許也有別人需要這個功能.


  廢話不多提,直接進入主題,要使用這個功能時,要先去加入一個COM元件的參考,Microsoft.Excel 11.0 Object.Library,將它加入參考後,就可以開始進行Coding的動作了,為了做基本功能的展示,所以做了一個簡易的UI.



加上一個OpenFileDialog即可,在Filter設定*.xls.其它就都是Code的部份.


接下來,就把調整格式的動作放入button_Click的事件內去做了.


        private void button1_Click(object sender, EventArgs e)
        {
            if (openFileDialog1.ShowDialog() == DialogResult.OK)
            {
                Microsoft.Office.Interop.Excel.Application excelApp = new Microsoft.Office.Interop.Excel.Application();
                Microsoft.Office.Interop.Excel.Worksheet excelWs;
                Microsoft.Office.Interop.Excel.Workbook excelWorkbook = excelApp.Workbooks.Open(openFileDialog1.FileName,
                Type.Missing, Type.Missing, Type.Missing, Type.Missing,
                Type.Missing, Type.Missing, Type.Missing, Type.Missing,
                Type.Missing, Type.Missing, Type.Missing, Type.Missing,
                Type.Missing, Type.Missing);

                excelWs = (Microsoft.Office.Interop.Excel.Worksheet)excelWorkbook.Worksheets.get_Item(1);//取得第一個sheet

                設定小大位置

                設定顏色

                調整儲存格格式

                ClearCom(excelWs);
                excelWorkbook.Close(true, Type.Missing, Type.Missing);

                ClearCom(excelWorkbook);
                excelApp.Workbooks.Close();
                excelApp.Quit();
                ClearCom(excelApp);

                excelWs = null;
                excelWorkbook = null;
                excelApp = null;
                MessageBox.Show("設定完成");
            }

        }


        static void ClearCom(object o)
        {
            try
            {
                System.Runtime.InteropServices.Marshal.ReleaseComObject(o);
            }

            catch { }
            finally
            {
                o = null;
            }

        }

 


這樣就完成Excel檔的格式調整了,當然還有其它未提到的參數屬性可以設,大家可以依自己的需求去找一下.


參考 :


MSDN : WorkSheet


MSDN : WorkBook


原始碼 : ExcelFormat.zip

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

Web Service一般都是直接呼叫使用,而以下的範例,則是加了Header,如果Header不對,則Client無法呼叫使用.


先從Web Service說起.


using System;
using System.Data;
using System.Web;
using System.Collections;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.ComponentModel;


namespace WSHeaderWS
{
    /// <summary>
    ///Service1 的摘要描述
    /// </summary>
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [ToolboxItem(false)]

    public class Service1 : System.Web.Services.WebService
    {
        public Service1()
        {
            if (Context.Request.Headers.Count < 2)
            {
                throw new Exception("Access Deny");
            }

            string[] AccID = Context.Request.Headers.GetValues("AccID");
            string[] AccPWD = Context.Request.Headers.GetValues("AccPWD");
            if (AccID == null || AccPWD == null)
            {
                throw new Exception("Access Deny");
            }

            if (AccID.Length < 1 || AccPWD.Length < 1)
            {
                throw new Exception("Access Deny");
            }

            if (AccID[0] != "123456" || AccPWD[0] != "654321")
            {
                throw new Exception("Access Deny");
            }

        }


        [WebMethod]
        public string HelloWorld()
        {
            return "Hello World";
        }

    }

}

 


從以上的Code可以看到,會先判斷Header存不存在,如果不存在,則Access Deny,如果存在,就判斷AccID及AccPWD是否正確,如果不正確,也是Access Deny.


而可以呼叫的WebMethod為HelloWorld().


這樣就做好Web Service端的Header部份的動作,而接下來Client就要使用這個WS.所以一定要先加入Web參考.


Client端的一般呼叫WS的方式很簡單.


try
{
    HW.Service1 ws = new WSHeaderCL.HW.Service1();
    MessageBox.Show(ws.HelloWorld());
}

catch (Exception ex)
{
    MessageBox.Show(ex.Message);
}

這個程式呼叫HelloWorld一定會跳Exception,因為沒有Header.


所以要加入Header才能執行,做一個Demo畫面,用來測試,如果當Header的值有誤時,會怎麼樣.



不含Header即是前一個範例,而接下來要做的是含Header的部份.


首先先產生一個ProxyWeb的Class(名字隨個人高興),因為我的WS是用HW命名,所以是HW.Service1


public class ProxyWeb : HW.Service1
{
     private System.Collections.Specialized.NameValueCollection headers = new System.Collections.Specialized.NameValueCollection();
    public ProxyWeb(string ID,string PWD)
    {
        headers.Add("AccID",ID);
        headers.Add("AccPWD", PWD);
    }

    protected override System.Net.WebRequest GetWebRequest(System.Uri uri)
    {
        System.Net.WebRequest request = base.GetWebRequest(uri);
        request.Headers.Add(headers);
        return request;
     }

}

有了這個Class後,就可以直接使用了


try
{
    ProxyWeb ws = new ProxyWeb(txt_ID.Text.Trim(),txt_PWD.Text.Trim());
    MessageBox.Show(ws.HelloWorld());
}

catch (Exception ex)
{
    MessageBox.Show(ex.Message);
}

用法一樣,只是在new ProxyWeb時,傳入ID及PWD,沒什麼特別的步驟.呼叫其它的WebMethod也不需再去寫另個Class或什麼的,用法都一樣,除非是另一個Web Service,才需要再寫一個Class.


測試結果,當然ID及PWD符合時,才會跳出HelloWorld,不然都是Access Deny.


而ProxyWeb除了可以加Header外,也可在這邊指定Web Service的Url,因為有些人的WS的Url是動態的,所以可以在ProxyWeb的這個Class指定就好.


 


這個的應用方式很多,不是像我這樣的簡易驗證,也可用來傳一些共用的固定值,例如UserID,我這只是Demo做法.


如果有更好的做法或寫法,也感謝各位的分享.


 


原始碼 :


Client : WSHeaderCL.zip


Web Service : WSHeaderWS.zip

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

  MD5的加密已經出來很長一段時間了,也不是什麼特別的新技術,寫這篇的用意也有點像是給自己的一個Note,畢竟加密的功能不常用,最多寫成一個Class,未來去呼叫就好,怕自己也會忘記,所以把這個寫下來.


  初步的UI設定如下:



H執行是單純加密,而Salt執行則是跑Salted Hash的動作.


單純加密比較不好,如果被人猜到加密方法是採MD5,也沒有Salted,那會風險會高一點,而Salt的效果,因為多了一個Value去加密,除了前面的都要猜到外,還要知道Salted Value才可以.而Salted Value就看大家要怎麼去設了,這裡也只是單純的Demo.


單純加密的部份,其實很簡單,程式碼如下 :


byte[] Original = Encoding.Default.GetBytes(txt_Source.Text); //將字串來源轉為Byte[]
MD5 s1 = MD5.Create(); //使用MD5
byte[] Change = s1.ComputeHash(Original);//進行加密
txt_Result.Text = Convert.ToBase64String(Change);//將加密後的字串從byte[]轉回string

很簡短的四行,就可以達成了,但個人還是喜歡用Salted,畢竟要加密,就要降低風險.而這方法加密後的結果如下 :


值 : 123456789


結果 : JfnnlDI7RTiF9RgfG2JNCw==


Salted Hash就比較複雜了一點 :


string salted = txt_Salt.Text.Trim(); //宣告變數,儲存Salted值
if (salted.Length == 0)//如果使用者沒給Salt值,那給預設
{
    salted = "t15t";
}

byte[] Original = Encoding.Default.GetBytes(txt_Source.Text);//將來源字串轉為byte[]
byte[] SaltValue = Encoding.Default.GetBytes(salted);//將Salted Value轉為byte[]
byte[] ToSalt = new byte[Original.Length + SaltValue.Length]; //宣告新的byte[]來儲存加密後的值
Original.CopyTo(ToSalt, 0);//將來源字串複製到新byte[]
SaltValue.CopyTo(ToSalt, Original.Length);//將Salted Value複製到新byte[]
MD5 st = MD5.Create();//使用MD5
byte[] SaltPWD = st.ComputeHash(ToSalt);//進行加密
byte[] PWD = new byte[SaltPWD.Length + SaltValue.Length];//宣告新byte[]儲存加密及Salted的值
SaltPWD.CopyTo(PWD, 0);//將加密後的值複製到新byte[]
SaltValue.CopyTo(PWD, SaltPWD.Length);//將Salted Value複製到新byte[]
txt_Result.Text = Convert.ToBase64String(PWD);//顯示Salted Hash後的字串

那個byte[]要放那個,那個byte[]要做什麼,很容易一不注意,就放錯個進去. 但這個加密後的如果如下:


值 : 123456789


Salted Value : goTest


結果 : erxXv9V5RYeCJaiF1z3yZ29UZXN0AA==


 


  因為這是目前仍無法反解回來的加密方式,所以就算DataBase的資料外流,也不用怕這些資料會被還原回來,而Salted Value就看大家要怎麼去"動"它囉.


原始碼 : encodeTest.rar

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

日前寫了一篇如何用GPS抓取目前所在,並回傳至資料庫儲存,這篇將會利用這些回報的資料,將它顯示在地圖上,這個做法有兩種,最簡單的就是直接傳值到Google Maps上.


 


舉例來說,當我們知道經緯度後,只要將數據套到以下網址即可.


http://maps.google.com/maps?q=25.048346%2c121.516396


在參數q=後面,就可以加上經緯度了.


25.048346是Latitude緯度


%2c是空格


121.516396就是Longitude經度了.


範例畫面 :


image


 


而另一種做法就比這個複雜一點,要使用Google API來做,首先,要使用google API就必需要有google的帳號,沒帳號是無法申請的,當有google的帳號後,就可以到http://code.google.com/apis/maps/signup.html開始申請了.


image


最下方My web site URL就輸入各位的URL囉,如果輸入的與執行google map api的URL不同,那就無法執行了.所以這個URL務必輸入正確, 輸入正確的URL並將上方的CheckBox打勾後,就可以按Generate API Key了,如果已經登入GOOGLE的,就不會再跳登入畫面,之後就會跳到另一個畫面,上面就有Key及Example Code了,當有了這些,就可以開始自己寫Code了.


 


基本上,因為主要是Demo用的,所以設計介面很簡單.


map1


上面就一個DropDownList,因為先前的範例資料的關係,先手動在ITEM上加上1跟2.


而下方的地圖,就跟申請API時的Example Code一樣. 原始碼如下 :


 


<%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" Inherits="_Default" %>

<!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 runat="server">
<title>GPS 位置地圖</title>
<script src="http://maps.google.com/maps?file=api&amp;v=2&amp;key=輸入你的Key"
type="text/javascript"></script>

<script src="http://www.google.com/uds/api?file=uds.js&v=1.0&key=輸入你的Key"
type="text/javascript"></script>

<script src="http://www.google.com/uds/solutions/localsearch/gmlocalsearch.js" type="text/javascript"></script>

<style type="text/css"> @import url("http://www.google.com/uds/css/gsearch.css");
@import url("http://www.google.com/uds/solutions/localsearch/gmlocalsearch.css");
</style>

<script type="text/javascript">
//<![CDATA[
function load(x,y,LocationName) {
if (GBrowserIsCompatible()) {
var map = new GMap2(document.getElementById("map"));
var point = new GLatLng(x,y);
map.setCenter(point, 16);
map.addOverlay(new GMarker(point));
map.addControl(new GLargeMapControl());
map.addControl(new GMapTypeControl());
map.addControl(new GScaleControl());
var lsc = new google.maps.LocalSearch();
map.addControl(new google.maps.LocalSearch() , new GControlPosition(G_ANCHOR_BOTTOM_RIGHT, new GSize(10,20)));
map.openInfoWindowHtml (map.getCenter(), LocationName);
}
}
//]]>
</script>
</head>
<body id="MainBody" runat="server">
<form id="form1" runat="server">
<div>
<asp:DropDownList ID="ddl_Location" runat="server" AutoPostBack="True" OnSelectedIndexChanged="ddl_Location_SelectedIndexChanged"
Width="500px">
<asp:ListItem>1</asp:ListItem>
<asp:ListItem Value="2">2</asp:ListItem>
</asp:DropDownList><br />
<br />
<div id="map" style="width: 500px; height: 400px">
</div>

</div>
</form>
</body>
</html>




 


只要將"輸入你的Key"的地方置換為你在Google MAP API申請到的Key即可.


 


   protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
if (Request.QueryString.HasKeys())
{
string longitude = Request.QueryString.Get("lon");
string latitude = Request.QueryString.Get("lat");
string LN = Request.QueryString.Get(Server.UrlDecode("LN"));
this.MainBody.Attributes.Add("onload", "load(" + longitude + "," + latitude + ",'" + LN + "')");
}
else
{
DataTable dt = GetLocation(ddl_Location.SelectedValue);
if (dt.Rows.Count > 0)
{
DataRow dr = dt.Rows[0];
this.MainBody.Attributes.Add("onload", "load(" + dr["Latitude"].ToString() + "," + dr["Longitude"].ToString() + ",'" + dr["updtime"].ToString() + "')");
}
}
}
}

protected void ddl_Location_SelectedIndexChanged(object sender, EventArgs e)
{
try
{
DataTable dt = GetLocation(ddl_Location.SelectedValue);
if (dt.Rows.Count > 0)
{
DataRow dr = dt.Rows[0];
this.MainBody.Attributes.Add("onload", "load(" + dr["Latitude"].ToString() + "," + dr["Longitude"].ToString() + ",'" + dr["updtime"].ToString() + "')");
}
}
catch (Exception ex)
{
Response.Write(ex.Message);
}


}

private DataTable GetLocation(string UID)
{
try
{
string strconn = "Data Source=localhost;Initial Catalog=GPSDB;User Id=GPSUser;Password=gpsuser;";
SqlConnection conn = new SqlConnection(strconn);
string strcmd = "select Latitude,Longitude,UpdTime from GPSDB..gpstrace where UID=@UID";
SqlCommand cmd = new SqlCommand(strcmd, conn);
cmd.Parameters.AddWithValue("@UID", UID);
SqlDataAdapter da = new SqlDataAdapter(cmd);
DataTable dt=new DataTable();
da.Fill(dt);
return dt;
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
}




 


如此一來,就大功告成了,但或許有些人會有些遺問,那麼MAP上,可以自訂一些東西,例如不給搜尋列,這都是可以做到的,可以參考Google Map API Examples,這裡就有很多詳細的說明.


 


感覺起來,GPS定位的想法部份,好像到此就沒了,但在這過程中也發現到,其實Google Map有出Mobile版的,而它的定位可不只是局限在GPS衛星訊號,而是可以用手機的訊號去定位,也就是說,他是透過手機與基地台之間的傳輸來計算出所在位置,這樣就可以不受到手機沒有GPS功能模組或收不到衛星訊號所限制,這個概念其實也不算新,記得幾年前的Run!PC雜誌上就有篇文章是在介紹這個的,採用的技術是Java.


 


不過不管如何,可以預見的是,這個的應用會愈來愈多元,誰說未來還要自己去用電腦下載圖資再更新到自己的GPS裝置上,裝置上的地圖永遠會是最新的,加上Street View,也不用去看那電腦畫出來的3D的道路圖了,或許3G或無線上網的普及,這些運用將會更廣泛.


 


參考資料 :


Google Map API Examples


Google Map Mobile


 


原始碼 : GPSMap.zip

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

  之前有寫了一篇"讓人知道你在那"的文章,希望能自己寫程式去運用GPS的定位功能,而這篇文章將會帶到兩個部份,第一個部份是PDA回傳資料時所用的Web Service,另一個就是PDA的GPS訊號抓取與轉換,而這部份並不是採用PAPAGO或其它的SDK,全部是採自己Coding的做法,在寫這個過程中,其實有點頭大,因為Coding的電腦是在室內,所以PDA是無法抓到GPS的訊號,所以只好從戶外拉了一條好長的GPS延長線到電腦邊,最後從抓到的經緯度丟到google map上去顯示,定出來的位置還算準確,確實定位出目前我的所在位置,另一個常在一般的衛星導航系統看到的情況,就是有時會亂跳,因為車子不可能在可行駛的道路以外,所以當經緯度偏離航道時,就會開始"亂跳",想辦法跳回最近的航道,這有好有壞,有時GPS是對的,位置真的不在道路上,但導航系統一直挑附近的道路跳,可是有時GPS訊號差,經緯度的誤差很大,明明在這條巷子,結果地圖顯示在另一條巷子裡,而導航程式本來就是用來帶領駕駛人行駛方向的,所以"車子"當然要在道路上.


 


  廢話了一堆,接下來就開始進入這次的主題,首先,為了儲存PDA所回傳的資料,所以在DataBase開了一個Table來儲存,資料結構大至上如下 :


資料庫 : GPSDB


TableName : GPSTrace


Columns :


UID (int,非null) 識別自動加1


Latitude (nvarchar(10),非null) 預設值 25.048346


Longitude(nvarchar(10),非null) 預設值 121.516396


UpdTime (DateTime,非null) 預設值 getdate()


資料結構大致如上,另外安全性的帳號問題,就各位依自己的喜好去設,配合GPS及下篇MAP展示做修改即可.


 


  此次的DEMO方向是PDA會回來Update資料,並不會做Insert的動作,所以只會儲存最新的所在位置,而第一次DB並沒有資料,所以Update一定會出錯,由於資料操作並非此次重點,因此先手動輸入兩筆資料進去.


SampleData


Web Service的部份也很簡單,只是很單純的Update的動作. 所以以下的Code只是為了達到Update的DEMO目地,其它安全性,例如Connection string不應直接寫在這裡面,這部份就依個人環境需求去改,如果再帶下去,文章會又臭又長,如果有需要,再額外開篇來討論.


[WebMethod]
public int UpdTrace(int UID,string Lat,string Long)
{
    string strconn = "Data Source=localhost;Initial Catalog=GPSDB;User Id=GPSUser;Password=gpsuser;"; //ID及PWD隨個人設定.
    SqlConnection conn = new SqlConnection(strconn);
    string strcmd = "update gpdtrace set Lat=@Lat,Long=@Long,UpdTime=getdate() where UID=@UID";
    SqlCommand cmd = new SqlCommand(strcmd, conn);
    cmd.Parameters.AddWithValue("@Lat", Lat);
    cmd.Parameters.AddWithValue("@Long", Long);
    cmd.Parameters.AddWithValue("@UID", UID);
    try
    {
        conn.Open();
        return cmd.ExecuteNonQuery();
    }
    catch (Exception ex)
    {
        throw new Exception(ex.Message);
    }
    finally
    {
        conn.Close();
        conn.Dispose();
        cmd.Dispose();
    }
}

 





 


PDA DB及Web Service好了,接下來要帶的就是PDA的GPS程式寫法(前面帶很快,因為我比較有興趣的是GPS的抓法^^),先大致看一下PDA的UI設計. 最上方的GPS Port是供使用者選擇GPS所使用的Port,因為每台Smart Device的GPS Port不見得相同,其它就都是唯讀的資訊,而資訊來源就是衛星訊號,從UI上看到的資訊只是衛星訊號的一部份,而衛星的訊號也只是抓其中的一種GPGGA出來用,其它還有GPGSV,GPGSA等等可以運用,衛星的訊號請參考GPS - NMEA sentence Information所提供的資料,在此就不再累述這些訊號一次.


 


 


 


 


 


 


 


 


 


而這次所運用的GPGGA訊號,基本上會抓到這類字串$GPGGA,095031.254,2501.9891,N,12133.8101,E,1,07,7.0,123.9,M,15.0,M,0.0,0000*74


每個逗號隔開視為一個欄位,而每個欄位值所代表的意義如下清單
























































Name



Example Data



Description


Sentence Identifier $GPGGA Global Positioning System Fix Data
UTC Time 095031.254 09:50:31
Latitude 2501.9891,N 北緯
Longitude 12133.8101,E 東經

Fix Quality:
- 0 = Invalid
- 1 = GPS fix
- 2 = DGPS fix


1 Data is from a GPS fix
Number of Satellites 07 7 Satellites are in view
Horizontal Dilution of Precision (HDOP) 7.0 Relative accuracy of horizontal position
Altitude 123.9,M 123.9 meters above mean sea level
Height of geoid above WGS84 ellipsoid 15.0,M 15.0,M
Time since last DGPS update 0.0 因為採GPS修正,不是DGPS,所以沒有資料
DGPS reference station id 0000  
Checksum *74 Used by program to check for transmission errors

 


瞭解了從衛星收到這字串的意義後,接下來就是怎麼去轉換經緯度的部份了,經度跟緯度的轉換方式都一樣,所以就拿其中一個來說明.


1. 2501.9891 /100 = 25.019891


2. 把小數點後的值從0.019891直接轉為19891


3. 再取小數點部份的值(19891/ 60)*10000=3315166


4. 由於第二步移了一個0,所以3315166轉小數時,要加一個0進去,所以=0.03315166


5. 所以緯度= 25+0.03315166=25.03315166


同樣的方式,算出經度=121.5635


*註此定出來的點是台北101


 


說完了GPS字串及轉換的方式,接下來就是Coding的部份.


1. 在下拉的ComboBox,供使用者選擇GPS Port的部份,先加入四個Port進去


this.cmbPort.Items.Add("COM1");
this.cmbPort.Items.Add("COM2");
this.cmbPort.Items.Add("COM3");
this.cmbPort.Items.Add("COM4");


 


2.加入一個Timer,Interval=1000


 


3.加入一個Serial Port


 


4. 在[開始]的Button加入一個click event. 如此,將GPS的Port打開,藉由Timer的Tick事件來更新資料


        private void btnStart_Click(object sender, EventArgs e)
        {
            serialPort1.PortName = cmbPort.SelectedItem.ToString();//依下拉選擇的com port設定Serial Port
            try
            {
                serialPort1.Open(); //開啟serial port
                txtPortStatus.Text = "";
            }
            catch (Exception ex)
            {
                txtData.Text = ex.Message;
            }
 
            if (serialPort1.IsOpen)//以下為依port的開關狀態來決定UI的動作
            {
                txtPortStatus.Text = "開啟";
                txtQuality.Text = "無";
                txtSTQty.Text = "0";
                txtSeaLevel.Text = "0";
                cmbPort.Enabled = false;
                btnStart.Enabled = false;
                btnStop.Enabled = true;
                timer1.Enabled = true;
            }
            else
            {
                timer1.Enabled = false;
                txtPortStatus.Text = "關閉";
                txtQuality.Text = "無";
                txtSTQty.Text = "0";
                txtSeaLevel.Text = "0";
                cmbPort.Enabled = true;
                btnStart.Enabled = true;
                btnStop.Enabled = false;
            }
        }

 







5. 在[結束]的Button加入一個click event. 如此,將GPS的Port關閉


        private void btnStop_Click(object sender, EventArgs e)
        {
            try
            {
                serialPort1.Close();//關閉serial port
                txtPortStatus.Text = "";
            }
            catch (Exception ex)
            {
                txtPortStatus.Text = ex.Message;
            }
 
            if (serialPort1.IsOpen)//以下為依port的開關狀態來決定UI的動作
            {
                txtPortStatus.Text = "開啟";
                txtQuality.Text = "無";
                txtSTQty.Text = "0";
                txtSeaLevel.Text = "0";
                cmbPort.Enabled = false;
                btnStart.Enabled = false;
                btnStop.Enabled = true;
                timer1.Enabled = true;
            }
            else
            {
                timer1.Enabled = false;
                txtPortStatus.Text = "關閉";
                txtQuality.Text = "無";
                txtSTQty.Text = "0";
                txtSeaLevel.Text = "0";
                cmbPort.Enabled = true;
                btnStart.Enabled = true;
                btnStop.Enabled = false;
                btn_upload.Enabled = false;
            }
        }
 

 





6. 在[回報]的Button加入一個click event. 如此回報目前所在位置


        private void btn_upload_Click(object sender, EventArgs e)
        {
            GPSWS.Service1 ws = new GPS.GPSWS.Service1(); //將之前做好的WS加入參考使用
            ws.Url="http://localhost/gpsws/service1.asmx";//設定Web Service的位址
            try
            {
                ws.UpdTrace(1, txtLatitude.Text, txtLongitude.Text);//上傳至資料庫
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

 


 





7. 在Timer的Tick事件加入判斷從Serial port的GPS訊息,並呈現於UI上


        private void timer1_Tick(object sender, EventArgs e)
        {
            if (serialPort1.IsOpen)
            {
                string GPSData = serialPort1.ReadExisting(); //將serialPort所取得的資料存到字串內
                txtData.Text = GPSData;//就是顯示在UI下方的那個大TextBox
                string[] gpsArr = GPSData.Split('$');//依$拆成多個字串陣列
                for (int i = 0; i < gpsArr.Length; i++)
                {
                    string strTemp = gpsArr[i];
                    string[] lineArr = strTemp.Split(',');
                    if (lineArr[0] == "GPGGA")//因為我們只用到GPGGA的訊息,所以其它的訊息不用
                    {
                        try
                        {
                            //Latitude 緯度
                            Double dLat = Convert.ToDouble(lineArr[2]);
                            dLat = dLat / 100;
                            string[] lat = dLat.ToString().Split('.');
                            string la =(((Convert.ToDouble(lat[1]) / 60)*10000)).ToString("#");
                            for (int a = 0; a < lat[1].Length; a++)
                            {
                                if (lat[1].Substring(a, 1) == "0")
                                {
                                    la = "0" + la;
                                }
                                else
                                {
                                    break;
                                }
                            }
                            Latitude = lat[0].ToString() + "." + la.Substring(0,6);
 
                            //Longitude 經度
                            Double dLon = Convert.ToDouble(lineArr[4]);
                            dLon = dLon / 100;
                            string[] lon = dLon.ToString().Split('.');
                            string lo = (((Convert.ToDouble(lon[1]) / 60)*10000)).ToString("#");
                            for (int b = 0; b < lon[1].Length; b++)
                            {
                                if (lon[1].Substring(b, 1) == "0")
                                {
                                    lo = "0" + lo;
                                }
                                else
                                {
                                    break;
                                }
                            }
                            Longitude = lon[0].ToString() + "." + lo.Substring(0,6);
 
                            //Display
                            txtLatitude.Text = Latitude;
                            txtLongitude.Text = Longitude;
                            txtSTQty.Text = lineArr[7]; //衛星數
                            txtSeaLevel.Text = lineArr[9];//海平面高度
                            switch (lineArr[6])//訊號品質
                            {
                                case "0":
                                    txtQuality.Text = "品質太差";
                                    break;
                                case "1":
                                    txtQuality.Text = "GPS fix(SPS)";
                                    break;
                                case "2":
                                    txtQuality.Text = "DGPS fix";
                                    break;
                                case "3":
                                    txtQuality.Text = "PPS fix";
                                    break;
                                case "4":
                                    txtQuality.Text = "即時性動態測量";
                                    break;
                                case "5":
                                    txtQuality.Text = "Float RTK";
                                    break;
                                case "6":
                                    txtQuality.Text = "Estimated";
                                    break;
                                case "7":
                                    txtQuality.Text = "手動輸入模式";
                                    break;
                                case "8":
                                    txtQuality.Text = "Simulation mode";
                                    break;
                                default:
                                    txtQuality.Text = "無";
                                    break;
                            }
                            btn_upload.Enabled = true;
                        }
                        catch
                        {
                            txtLatitude.Text = "GPS 訊號不足";
                            txtLongitude.Text = "GPS 訊號不足";
                            txtQuality.Text = "無";
                            txtSTQty.Text = "0";
                            txtSeaLevel.Text = "0";
                            btn_upload.Enabled = false;
                        }
                    }
                }
            }
            else
            {
                txtLatitude.Text = "COM Port 已關閉";
                txtLongitude.Text = "COM Port 已關閉";
                txtQuality.Text = "無";
                txtSTQty.Text = "0";
                txtSeaLevel.Text = "0";
                btn_upload.Enabled = false;
            }
        }

 





 


  以上的Code就完成了GPS的訊號抓取,轉換,呈現與回報資料庫的動作了,當然,一定可以寫的更好,做更多的運用,使用更多來自衛星的訊息,而這些只是用來實作出用VS2005 C#來達成這個想法,所以看起來較簡略,其它就依各自的需求與想法去延伸.當初花了一點時間在找經緯度轉換的公式,而這也是這整個的關鍵所在,所以這一整個程式碼部份,光看GPS訊號相關,其實就從Serial Port抓訊息,轉換與呈現,原本以為很複雜,結果還好,很簡單,下篇文章就是如何運用google API來呈現所回傳的所在位置.


 


參考資料 :


GPS - NMEA sentence Information


NMEA Data


 


原始碼 :


PDA : GPS.zip


Web Service : GPSWS.zip

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

  最近腦海中一直有個構想,從物流車到四川地震,兩碼子不同的事,但卻有個相同的東西,就是讓人知道你在那,先從四川地震來說起,這次部份獲救的人是透過GPS來掌握所在位置,才能順利獲救,試想,如果有一個SOS回報網站,將你目前的經緯度傳回SOS救援中心,他們即能從地圖上定位出來,並前往救援,這個並不是一個新的概念,目前中興保全的MiniBond即有此服務,但局限在使用他們即有的設備,如果能用即有的手機(當然要先含GPS的功能),透過SMS或GPRS\3G..等發送資訊回公司,相信接受度會更高,畢竟沒人喜歡身上又多背了一根狗骨頭. 而物流車為什麼也需要讓人掌握目前位置所在? 答案也很簡單,即時的掌握車輛所在,如果有客戶需要去收貨,臨時就可以調動附近的車輛去收,也不用一台一台的去確認那一台離最近.


 


  其實這種東西已經應用在很多地方了,像汽車防盜,當車子被盜時,會啟動回報系統,告知目前車輛所在,或是部份計程車行也採用,好在電話叫車時調度車輛,但這些目前都局限在"專用設備",無法用有GPS功能的PDA或手機來回報所在,所以在這個構想下,腦海中大概有個藍圖,所在位置的地圖呈現可用google map來做,所以沒有圖資及一堆的MAP相關資料及程式碼要去處理,加上一個儲存回報位置圖的資料庫,一台有GPS又可上網的裝置,只要抓到經緯度回報到Server去處理即可,所以簡單的分三個部份去處理.


 


1. PDA透過GPS抓取經緯度,回報所在位置.


 


2. Web Service接收PDA所回傳的資料並儲存.


 


3. Web呈現,抓取資料庫內的經緯度,將它用地圖呈現所在.


 


  這樣畫分下來就很簡單了,可以分動作去完成這個構想,這篇只是一個構想的開始,後續文章會再透過程式範例及相關說明的方式道出怎麼去完成它.


 


待續..

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

  在第一篇的時候,已經講到如何抓取桌面上的畫面,所以這篇所要做的,就是將抓下來的畫面透過TCP的方式,傳送出去與呈現,這部份會用到Thread的方式去做TCPListener.


 


  我的做法是用Timer的方式,每隔一段時間傳送一次,所以會用到Timer這個物件,時間設太短只是加重負擔,太長,畫面斷續的會更明顯,所以這個視情況調整,也不見得要用Timer的方式,我只是用它來觸發傳圖的動作.


 


   1:          private void timer1_Tick(object sender, EventArgs e)
   2:          {//timer 的Tick事件
   3:              SendImg();
   4:          }
   5:   
   6:          private void SendImg()
   7:          {
   8:              try
   9:              {
  10:                  TcpClient tcpc = new TcpClient(txt_IP.Text.Trim(), 5657); //txt_IP是要傳送的目的IP,看要傳到那一台電腦去,用的Port是5657
  11:                  NetworkStream tcpStream = tcpc.GetStream();
  12:                  byte[] SC = GetScreen(); //GetScreen就是前一篇所介紹的
  13:   
  14:                  tcpStream.Write(SC, 0, SC.Length);
  15:                  tcpStream.Flush();
  16:                  tcpStream.Close();
  17:                  tcpStream.Dispose();
  18:                  tcpc.Close();
  19:              }
  20:   
  21:              catch (Exception ex)
  22:              {
  23:                  txt_Msg.Text = ex.Message; //txt_Msg是TextBox,用來顯示錯誤訊息的.怎麼呈現,看各位想法
  24:              }
  25:          }

 





 


  在傳送的部份,較為簡單,把圖給傳送出去就好,而接收部份,就較複雜一點,這部份有用到Thread的方式去運作,將接收到的圖面呈現在PictureBox上,此接收與呈現圖面的原始碼如下:


 


   1:          private Thread thImg;
   2:          private TcpListener tcpImg;
   3:          public bool listenerRun = true;
   4:          //listenerRun為true,表示可以接受連接請求,false則為結束程式
   5:          public Form1()
   6:          {
   7:              InitializeComponent();
   8:              thImg = new Thread(new ThreadStart(ListenImg));
   9:              thImg.Start();
  10:          }
  11:   
  12:          private void ListenImg()
  13:          {
  14:              try
  15:              {
  16:                  tcpImg = new TcpListener(System.Net.Dns.GetHostAddresses(System.Net.Dns.GetHostName())[0], 5657);//在5657埠新建一個TcpListener物件
  17:                  tcpImg.Start();
  18:   
  19:                  while (listenerRun)//開始監聽
  20:                  {
  21:                      Socket s = tcpImg.AcceptSocket();
  22:                      NetworkStream ns = new NetworkStream(s);
  23:   
  24:                      int size = 100000;
  25:   
  26:                      byte[] buff = new byte[size];
  27:                      int read = -1;
  28:                      read = ns.Read(buff, 0, size);
  29:   
  30:                      if (read > 0)
  31:                      {
  32:                          MemoryStream ms = new MemoryStream(buff, 0, size);
  33:                          this.Invoke(new SetImg(Set_Img),new object[] {Image.FromStream(ms)}); //更新圖面資訊
  34:                          this.Invoke(new SetMsg(Set_Msg), new object[] {s.RemoteEndPoint.ToString() }); //更新連線IP
  35:                          ms.Dispose();
  36:                          this.Invoke(new SetErr(Set_Err), new object[] {""});
  37:                      }
  38:                  }
  39:              }
  40:              catch (System.Security.SecurityException ex)
  41:              {
  42:   
  43:                  this.Invoke(new SetErr(Set_Err), new object[] { ex.Message });
  44:                  
  45:              }
  46:              catch
  47:              {
  48:   
  49:              }
  50:          }
  51:   
  52:          private delegate void SetImg(Image bmp);
  53:          private void Set_Img(Image bmp)
  54:          {
  55:              pictureBox1.Image = bmp; //將圖呈現出來
  56:          }
  57:   
  58:          private delegate void SetErr(string err);
  59:          private void Set_Err(string err)
  60:          {
  61:              txt_Msg.Text = err; //顯示錯誤訊息
  62:          }
  63:   
  64:          private delegate void SetMsg(string msg);
  65:          private void Set_Msg(string msg)
  66:          {
  67:              txt_ImgFrom.Text = msg; //顯示畫面來自那個IP
  68:          }

 





 


  這樣子就完成了遠端桌面的抓取與傳送/呈現的功能了,但這還有很多待改善的部份,希望各位高手能提供想法,大家一起交流~


 


註 : 此程式所會用到的命名空間如下


using System.Threading;
using System.Net.Sockets;
using System.IO;


 


參考資料 :


MSDN Thread 類別


MSDN System.Net.Sockets 類別


MSDN System.IO 命名空間


 


原始碼 : 下載

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

  這幾天突發奇想,想寫個程式能把另一台電腦上的桌面即時的傳輸到我的電腦上,可以藉此知道另一台電腦的狀況,所以就開始著手Study這個東西,這個功能會用到一些技術,覺得這些技術也不錯玩,所以把這個文章分幾篇來寫. 最後的程式畫面就會如下:




底下的那個畫面就是來自192.168.1.60


 


  即然要把桌面的畫面傳到另一台電腦去顯示,那麼首先就要先能抓取到目前的桌面畫面,因為網路傳輸量的關係,所以圖檔也不能太大,也因此這部份額外抓出來談.



   1:          private byte[] GetScreen()
   2:          {
   3:              int Height = System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height;//抓取桌面的高度
   4:              int Width = System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width;//抓取桌面的寬度
   5:              Bitmap screenshot = new Bitmap(Width, Height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
   6:              Graphics graph = Graphics.FromImage(screenshot);
   7:   
   8:              graph.CopyFromScreen(0, 0, 0, 0, new Size(Width, Height), CopyPixelOperation.SourceCopy);
   9:   
  10:              int FixWidth = 800; //設定新圖檔的解晰度,寬度
  11:              int FixHeight = Convert.ToInt16((Decimal.Parse(FixWidth.ToString()) / Decimal.Parse(Width.ToString())) * Height); //計算高度
  12:              Bitmap bmp = new Bitmap(screenshot, new Size(FixWidth, FixHeight));
  13:              MemoryStream ms = new MemoryStream();
  14:              bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg); //把圖檔用新的解晰度去儲存
  15:              screenshot.Dispose();
  16:              graph.Dispose();
  17:              bmp.Dispose();
  18:              return ms.GetBuffer();
  19:          }


 






 


  這段程式碼是這整個功能的一部份,主要是用來回傳把桌面用byte[]來回傳,而抓圖部份則是在3~8行就完成了,而10~14則是改變圖檔的解晰度去儲存,如果用原始大小,似乎大了點,所以調一下解晰度再回傳,可以節省一點頻寬,或許這時有人會想問,那把它壓縮啊! 答案是不行的,問題說明請參考鄭子璉的 : GZipStream/DeflateStream 在壓縮二進位檔會造成檔案放大


 


  而第3,4行呢,則可依個人需求去抓取桌面,比如說,要的只是桌面,不含工具列,或是跟此範例一樣,是含工具列的,當然也可以抓ActiveWindow,那第8行就也要改一下,才會抓對位置.


 


  所以呢,除了改變解晰度及色彩外,似乎沒有其它可用的壓縮方案來進行壓圖的動作,抓取桌面或部份,就看個人需求,而這段Code就可以把圖面用byte[]回傳了,下次就會是說明,如何用TCP來把它傳送出去,及Client端怎麼去接收與顯示.


 


參考資料 :


MSDN Bitmap 類別


MSDN Graphics 類別


MSDN Screen 類別


鄭子璉 GZipStream/DeflateStream 在壓縮二進位檔會造成檔案放大

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

  最近又再度試著把公司的ERP系統從VS2002升級至VS2005,運用升級精靈的過程中是沒遇到什麼問題,也沒花多少時間,讓我一時有了"無痛升級"的感覺,執行試跑後,痛開始來了,怎麼輸入了一個代號,沒有自動帶出名字,從Debug模式中去看,事件有觸發啊,怎麼會沒帶出名字,只好Step by Step的一行一行跑,結果發現了一行很玩味的情況.


 


以下這幾段程式碼為重現問題範例 :


        private void Form1_Load(object sender, System.EventArgs e)
{
DataTable dt = new DataTable("Test");
dt.Columns.Add("AA",typeof(string));
dt.Columns.Add("BB", typeof(string));
for (int i = 1; i < 6;i++ )
{
DataRow dr = dt.NewRow();
dr["AA"] = "A" + i.ToString();
dr["BB"] = i.ToString();
dt.Rows.Add(dr);
}
DataGrid1.DataSource = dt;
dataGridTextBoxColumn1.TextBox.Validated+=new EventHandler(TextBox_Validated);
}

void TextBox_Validated(object sender, EventArgs e)
{
int r = DataGrid1.CurrentRowIndex;
MessageBox.Show(DataGrid1[r, 0].ToString()+" "+ ((TextBox)sender).Text.Trim());
}


 


在表單載入時,先放入測試資料到DataGrid1裡,所以執行後,會看到下面這個畫面:



 


  VS2002與VS2005在這表單軟入部份都沒有問題,而差異點在TextBox_Validated這個事件內.如果我把左上第一個欄位的值改為BB後,VS2002跟VS2005所MessageBox.Show出來的結果是不同的.


 


VS2002 所 Show的值:


DataGrid1[r,0].ToString()  ==> A1


((TextBox)sender).Text.Trim() ==> BB


 


VS2005 所 Show的值:


DataGrid1[r,0].ToString()  ==> BB


((TextBox)sender).Text.Trim() ==> BB


 


  同樣的DataGrid1[r,0].ToString()在同樣的Validated事件內,抓到的值是不一樣的,VS2002所帶出來的值也因此,我原本會帶出的名稱,為此因素,所以名稱就沒帶出來了.


 


  另一個問題也很奇怪,在VS2002內,在Cell內,不論是用方向鍵去換Cell,還是Tab鍵,Enter鍵,還是滑鼠去點任一Cell,都會觸發Validated事件,而VS2005大部份也會觸發,只有"Enter鍵"不會Validated事件,這讓人很不解,程式碼都一樣,但結果是不同的.


 


看來後面還會有不少的痛......

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

  由於目前有些平台仍是在.Net 1.X上面,雖然GZipStream可以壓縮資料流,但它必需在.Net 2.0以上的平台才能用,但也不代表.Net 1.X就只能乖乖的認命,接受這個結果,其實在.Net 1.X上,也有一個方法可以做到資料流的壓縮,微軟有提供一個物件DataSetSurrogate來協助開發人員不用花太多的時間在Coding上,就可以達成目標.


  


  仔細去看DataSetSurrogate的原始碼後,大至上就可以了解到它是怎麼去"簡化"資料量,怎麼運用多個Array List及HashTable去儲存DataSet裡的資料,雖然壓縮率沒有GZipStream來的高,但也算很優了,而運用也相當的簡單,不用寫太多的程式碼在上面,以下就是程式碼部份的簡介 :


 


  首先,要去下載DataSetSurrogate的這個檔案,下載解壓後,可以看到VB及CSharp的目錄,這兩個目錄都一樣,只是一個是用VB寫的,另一個是C#,而這目錄以下,有三個目錄 : DataSetSurrogate,DSServer,TestSurrogate.而TestSurrogate為測試的專案,DSServer是測試專案的DataAcess的部份,除了要測試,不然可以不用管它,因為我們要用的是DataSetSurrogate,剛解壓是沒有編譯過的,所以要先Complier這個專案,才能得到我們要用的DLL物件.


 


當我們得到DataSetSurrogate這個物件後,就可以拿到我們的專案裡用,當然,不論是Client還是Server,都要記得先將DataSetSurrogate加入參考才能用,以下就是程式範例 :


Server Side :


using System.Data.SqlClient;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;



[WebMethod]
public byte[] GetDSBinary()
{
try
{
DataSetSurrogate dss=new DataSetSurrogate(LoadData().Copy());
BinaryFormatter bf = new BinaryFormatter();
MemoryStream ms=new MemoryStream();
bf.Serialize(ms,dss);
return ms.GetBuffer();
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
}



private DataSet LoadData()

{

try
{
DataSet ds=new DataSet();

//從資料庫取得資料

return ds;

}

catch (Exception ex)
{
throw new Exception(ex.Message);
}
}

 





 


Server端的Web Service就只有這樣,就可以了,所以程式碼部份,真的很少. 而Client端的部份,也是一樣,並不多.


Client Side :


using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
//using 剛剛做好的WebService

 




        private void button1_Click(object sender, System.EventArgs e)
{
WS.Service1 w=new WSGet.WS.Service1();//new 剛剛的WebService
try
{
dataGrid1.DataSource=null;
this.Cursor=Cursors.WaitCursor;
byte[] bt=w.GetDSBinary();

MemoryStream ms=new MemoryStream(bt);

BinaryFormatter bf=new BinaryFormatter();
object obj=bf.Deserialize(ms);
DataSetSurrogate dss=(DataSetSurrogate)obj;
dataGrid1.DataSource=dss.ConvertToDataSet().Tables[0];
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
finally
{
this.Cursor=Cursors.Default;
}
}
 

 





Client端這樣也就OK了,測試了一下壓縮量的差異,結果如下表 :


















  壓縮前 壓縮後 壓縮比
GZipStream 18,368,067 bytes 4,445,278  bytes 75%
DataSetSurrogate 6,563,150 bytes 2,872,540 bytes 45%

雖然有20%的差異,但也是不錯了,不知道GZipStream+Serialize+DataSetSurrogate會是什麼樣的結果~(預計效果可能不大,依據微軟的說法,.Net 2.0的BinaryFormatter已將DataSet的內容以精簡化之二進位制格式化,所以不需再用DataSetSurrogate)


 


  之所以會去翻出.Net 1.X的舊平台解決方案,主要也是因為現在公司的Client端部份仍是.Net1.X,無法在2.0上運作,雖然Server端是2.0的,也是無法使用GZipStream,但說到這裡,有一個重點出現了,我嘗試過用DataSetSurrogate放在.Net2.0的執行,從2.0的Server端丟回到1.X的Client端,結果會跳出錯誤訊息"可能原因為版本不相符。型別 System.Globalization.CompareInfo 有 2 個成員,但還原序列化後的成員數目卻為 3。".把Server設為1.X就OK了,初步判斷,應該是BinaryFormatter的問題,因為.Net1.X與2.0的不同,如上所述"Net 2.0的BinaryFormatter已將DataSet的內容以精簡化之二進位制格式化",而1.X的並沒有,所以可能因此,在這部份上造成我在Deserialize時,會發生錯誤,這個問題目前還沒解決,就看看大家有沒有答案囉.


 


參考資料 :


ADO.NET 1.x Dataset 序列化問題探討


增進資料集序列化和遠端效能


下載 : DataSetSurrogate

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

  不知道有多少人有遇到跟我一樣的問題,就是Web Service的資料回傳量太大了,如果都是走區域網路的話,除非量很大,不然還感覺不太出來,可是,如果是透過ADSL的頻寬的話,那就很驚人了,以30MB的資料量來說,透過2M/512K的網路上傳,加上網路品質的損耗以65%來看,也大概要花738秒左右來上傳. (30*1024)/((512*0.65)/8)=738.46. 這樣的時間,看起來很嚇人,何況在上傳滿載時,下載的頻寬可能剩40%不到,如果今天能壓縮這資料量,讓資料量剩25%左右,那上傳時間就只要185秒左右,這樣的差異就很大了,如果今天我們採用的是n-tier架構,Client是透過Web Service在要資料,那要怎麼去壓縮這傳輸過程中的資料? 這時就可以拿GZipStream出來用了.


 


  起初,GZipStream只是被我拿來當檔案壓縮用的,剛好這段時間遇到公司ERP系統效能調校,突然產生了一個想法,如果我能拿它來壓縮資料,那傳輸的資料量不就小很多,尤其是XML的文字檔,效果一定更明顯,就在這個念頭下,著手開始寫了測試的程式,在這裡,我做了兩種不同的做法,一個是純壓縮,不Serialize,另一種做法是壓縮與Serialize,而這兩種做法,是會產生些微的資料量差異.


 


在提到程式內容之前,先說明這做法所造成的資料量差異,詳見下表 :
















正常資料量



壓縮後



壓縮+Serialize



18,368,067 Bytes



4,755,570 Bytes



4,445,278 Bytes



17,937.57 Kb



4,644.11 Kb



4,341.09 Kb



 


 


接下來,就是程式碼部份,分兩個部份說明:


1 .純壓縮 :


WebService Side :


using System.IO;
using System.IO.Compression;



[WebMethod]
public byte[] getZipData()
{
DataSet ds = LoadData().Copy();
MemoryStream unMS = new MemoryStream();
ds.WriteXml(unMS);

byte[] bytes = unMS.ToArray();
int lenbyte = bytes.Length;

MemoryStream compMs = new MemoryStream();
GZipStream compStream = new GZipStream(compMs, CompressionMode.Compress, true);
compStream.Write(bytes, 0, lenbyte);

compStream.Close();
unMS.Close();
compMs.Close();
byte[] zipData = compMs.ToArray();
return zipData;
}

private DataSet LoadData()
{//產生測試資料用
DataSet ds = new DataSet();
DataTable dt = new DataTable("Test");
dt.Columns.Add("ProID",typeof(int));
dt.Columns.Add("ProName", typeof(string));
dt.Columns.Add("CreateTime", typeof(DateTime));
dt.Columns["ProID"].AutoIncrement = true;
for (int i = 0; i < 100000; i++)
{
DataRow dr = dt.NewRow();
dr["ProName"] = Guid.NewGuid().ToString();
dr["CreateTime"] = DateTime.Now.ToString();
dt.Rows.Add(dr);
}
ds.Tables.Add(dt);
ds.AcceptChanges();
return ds;
}

 


Client Side :


using System.IO.Compression;
using System.IO;

private void btn_ZipGet_Click(object sender, EventArgs e)
{
try
{
WS.Service1 wss = new WSZipDemo.WS.Service1();//WebReference
byte[] da = wss.getZipData();

MemoryStream input = new MemoryStream();
input.Write(da, 0, da.Length);
input.Position = 0;
GZipStream gzip = new GZipStream(input, CompressionMode.Decompress, true);

MemoryStream output = new MemoryStream();
byte[] buff = new byte[4096];
int read = -1;
read = gzip.Read(buff, 0, buff.Length);
while (read > 0)
{
output.Write(buff, 0, read);
read = gzip.Read(buff, 0, buff.Length);
}
gzip.Close();
byte[] rebytes = output.ToArray();
output.Close();
input.Close();

MemoryStream ms = new MemoryStream(rebytes);
DataSet ds = new DataSet();
ds.ReadXml(ms);
dataGridView1.DataSource = ds.Tables[0];
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}

 


2. 壓縮+Serialize


Web Service Side :


using System.IO;
using System.IO.Compression;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;



[WebMethod]
public byte[] getZipData()
{
DataSet ds = LoadData().Copy();
ds.RemotingFormat = SerializationFormat.Binary;
BinaryFormatter ser = new BinaryFormatter();
MemoryStream unMS = new MemoryStream();
ser.Serialize(unMS, ds);

byte[] bytes = unMS.ToArray();
int lenbyte = bytes.Length;

MemoryStream compMs = new MemoryStream();
GZipStream compStream = new GZipStream(compMs, CompressionMode.Compress, true);
compStream.Write(bytes, 0, lenbyte);

compStream.Close();
unMS.Close();
compMs.Close();
byte[] zipData = compMs.ToArray();
return zipData;
}

private DataSet LoadData()
{//產生測試資料用
DataSet ds = new DataSet();
DataTable dt = new DataTable("Test");
dt.Columns.Add("ProID",typeof(int));
dt.Columns.Add("ProName", typeof(string));
dt.Columns.Add("CreateTime", typeof(DateTime));
dt.Columns["ProID"].AutoIncrement = true;
for (int i = 0; i < 100000; i++)
{
DataRow dr = dt.NewRow();
dr["ProName"] = Guid.NewGuid().ToString();
dr["CreateTime"] = DateTime.Now.ToString();
dt.Rows.Add(dr);
}
ds.Tables.Add(dt);
ds.AcceptChanges();
return ds;
}

 


Client Side :


using System.IO.Compression;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;



private void btn_ZipGet_Click(object sender, EventArgs e)
{
try
{
WS.Service1 wss = new WSZipDemo.WS.Service1();//WebReference
byte[] da = wss.getZipData();

MemoryStream input = new MemoryStream();
input.Write(da, 0, da.Length);
input.Position = 0;
GZipStream gzip = new GZipStream(input, CompressionMode.Decompress, true);

MemoryStream output = new MemoryStream();
byte[] buff = new byte[4096];
int read = -1;
read = gzip.Read(buff, 0, buff.Length);
while (read > 0)
{
output.Write(buff, 0, read);
read = gzip.Read(buff, 0, buff.Length);
}
gzip.Close();
byte[] rebytes = output.ToArray();
output.Close();
input.Close();

MemoryStream ms = new MemoryStream(rebytes);
BinaryFormatter bf = new BinaryFormatter();
object obj = bf.Deserialize(ms);
DataSet ds = (DataSet)obj;
dataGridView1.DataSource = ds.Tables[0];
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}

 


  這兩種做法只是在部份的程式碼不一樣,但其它大多相同,如果是方法一,在WebService壓縮後Return,Client收到資料後,解壓縮即可,方法二則多了Serialize這部份,所以在WebService這邊,先Serialize,再壓縮,Client端收到後,先解壓縮再Deserialize. 不過,這個壓縮後的量,是很讓人滿意,如果在頻寬有限,又需要傳輸大量資料時,這個方法可以考慮看看. 因為這個是.Net 2.0以後才有的東西,公司現在的ERP還在.Net 1.0,所以.........殘念~


 


2008/04/29 補充 :


  最近有網友看到這篇文章,有些問題問我朋友,而朋友再來轉問我這個問題(真巧,網友問我朋友,我朋友再來問我),而提出的問題是,壓縮跟解壓是否很耗用CPU的效能? 答案是,不可能不用到CPU的效能,但它也不致於到"很耗用"的地步. 而這耗用率只能視設備狀況來判斷,以我的環境來說,Client端的電腦是P4 3GHT, 上面的這個例子跑的時候有個瞬間最高47%,不到一秒的時間,如果是未壓縮版的,瞬間最高為37%左右,所以大概多個10%吧.


 


  或許有人還是有疑慮怎麼可以增加CPU的Loading呢,這樣就不好了,這時我們就要換個角度來思考這個問題了,"效能成本"所在為何,我們的Bottleneck在那.CPU在科技的進步下,雙核四核的都推出了,CPU的效能是快速的在倍增中,而我們的網路呢? 絕大多數區域還在100MB,部份Server與Server是1G在連,而ADSL呢? 10M/1M或者是只能2M/512K,因為申請不到更快的頻寬,如果要更快,每月的費用就更高, 注意囉,上傳是1MB或512K哦,而且是多人共用的,所以就目前國內的網路環境來看,ADSL要到100M/100M,似乎還要很長的一段時間,而這段時間的CPU,也不知道已經成長到幾核心了,兩者之間怎麼取捨,就看大家的看法囉.


 


2008/5/30 補充


  因為一些朋友對這個方式感到有興趣,也在網路上找了一些文章,可是卻又發現了一些疑問,到底這個壓縮技術能用在什麼樣的情況下. 其中一位朋友傳給了我一個網址.Net DataTable 大量資料壓縮加密實測,也是說明用壓縮的方式來減少流量,一些測試結果也很詳盡,有興趣的人建議參考,只是朋友在他的文章開頭的地方看到 [開發 Web 或分散式系統],這是否代表Web網站也可以用? 這個答案當然是沒用的,試想,我們在Web Server壓縮,Client端用IE或Firefox怎麼解壓縮,網頁是無法用Gzipstream的方式來解壓縮來減少Web Server到Client的資料量,能壓一定要能解才有用,所以就不用再去列出那些可以及那些不行了,畢竟這還關係的架構上的問題,所以應用的關鍵就是"能壓要能解才能用".


  另一個問題就是選擇性的使用,壓縮的動作勢必用到系統的效能,如果全面性100%的採用壓縮,當使用者多,或操作頻率高時,資料量大,系統效能也會變的更加吃重,所以必需挑選幾個關鍵的傳輸來壓縮即可,不用連1K不到的資料量也在壓縮,除非Server很猛的,那就另當別論. 依我的情況,我會挑選幾個使用者的查詢作業來壓,依目前手頭上的分析資料來看,有個查詢作業使用頻率較高,資料量也是驚人,最高的資料量就一次高達33MB,平均起來跟其它作業比較,這作業的資料量佔了不小的比率,光這作業一天平均傳輸量650MB,壓縮後只剩162.5MB在傳,所以只壓縮這個作業的查詢動作,就可明顯的改善ADSL的頻寬瓶頸. 所以要視情況去選擇要壓縮的作業.


 


參考資料 :


MSDN GZipStream 類別


MSDN BinaryFormatter 建構函式 ()

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

  記得VS2005剛上市時,公司就極力想把目前在用的ERP系統升級上來,其中一個新的ClickOnce功能,也被納入期待能採用的更版技術,可能主管們在TechEd聽到多簡單,多好用等,所以才想要籍由此升級機會,把更版的方式改為ClickOnce,在這需求下,除了原本的程式碼升級痛苦外,又多了一個Study ClickOnce.


 


  剛碰時,確實很簡單,只要在專案屬性的地方設一設,就可以部屬了,簡單的幾個設定,就可以決定程式是要離線可使用(會出現在開始-程式集下),或是必需到網頁上點取才可以執行,或是更版的時機點.



 


  如果這麼簡單,就能搞定公司的使用者需求,那我就輕鬆了,可惜並不行,這個版本的管控與RollBack是公司很Care的地方,而ClickOnce的更版時機有兩個,一個是執行前,先檢查新版,有就先安裝,安裝好後,再執行程式,另一個是先執行,後檢查,如果有新版,下次再執行程式時,才會進行安裝.



 


  那麼執行過程中,有程式更版要怎麼辦? 使用者並不會那麼安份的關電腦才下班,所以隔天可能還是用到舊版的,或是程式有Critical Bug,不能等,必需即刻更新,那要怎麼辦,這個介面上並沒有辦法設定啊,這時就必需自行寫程式碼來達成.


 


try
{
if (!(ApplicationDeployment.IsNetworkDeployed)) return; //確定此程式是否為ClickOnce所部署的程式
System.Deployment.Application.ApplicationDeployment obj = ApplicationDeployment.CurrentDeployment;//取得目前使用者的版本資訊
if (ApplicationDeployment.CurrentDeployment.CheckForUpdate())//比對伺服器上的版本
{
if (MessageBox.Show("發現新的版本,是否確定更版", "更版通知", MessageBoxButtons.YesNo, MessageBoxIcon.Information) == DialogResult.Yes)
{
obj.UpdateProgressChanged += new DeploymentProgressChangedEventHandler(obj_UpdateProgressChanged);
obj.UpdateCompleted += new AsyncCompletedEventHandler(obj_UpdateCompleted);
obj.UpdateAsync();
}
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}

private void obj_UpdateProgressChanged(object sender, DeploymentProgressChangedEventArgs e)
{
Progress_Update.Value = e.ProgressPercentage;
Application.DoEvents();
}

private void obj_UpdateCompleted(object sender, AsyncCompletedEventArgs e)
{
Application.Restart();
}

 


  以上這段程式碼就是自行寫程式去檢查版本並進行更版,不過或許有人對ApplicationDeployment.IsNetworkDeployed這行有點疑問,為什麼要加這行判斷?這程式本來就是用ClickOnce來部署的啊,多這判斷不是多此一舉? 剛開始,我也是沒有加這行Code,之所以會加上去,是我在開發階段時,並不是透過ClickOnce來部署的,所以每次在開發時,跑到這裡就會出問題,加了這行,才不會往下跑.


 


  看樣子,更版的問題似乎解決了,但還有個RollBack問題要解決,ClickOnce基本上就具有RollBack的功能,在新增移除程式的地方,使用者可以自行選擇是否要回到前一版還是移除程式,如果不讓使用者回到前一版,可以在當初發行時,指定最小版本,如果發行的最新版本=最小版本,那麼使用者只能移除,無法選擇回到前一版. 但如果要強制使用者RollBack,總不可能一個一個請使用者自行去新增移除程式的地方,自己去RollBack吧,而且之前如果有設最小版本=發行版本,使用者也無法RollBack,這個問題也讓我頭痛一段時間,最後想到的一個可行方法,就只能重新發佈前一版的程式,只是版次是加1號,所以原本是Ver 2要回到Ver 1,用Ver 1的程式碼重新發行,其版次設為Ver 3,最小版次為Ver 3,這樣使用者就會更到前一版的程式碼,而且也不能RollBack到Ver 2.


 


  ClickOnce是個不錯玩的功能,但要把它用在適合的地方,才能發揮它的功能,畢竟它也是需要一台Web Server,才能把這更版網頁放上去,並不是每間公司都願意放一台Web Server,或許是可以整併到DataBase Server那台,但還是有人不願意,希望DB單純化,所以反而Windows Installer會比較好用,反正用其它的寫法,同樣可以達成同樣的目標,何必為了更版多了一個Web,而且ClickOnce還是有部份的限制,只有Windows Installer才可以達成,所以除了自己寫的程式,目前還沒遇過其它ClickOnce的作品.


 


參考資訊 :


MSDN ClickOnce部署概觀


MSDN ApplicationDeployment 類別

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

  一般來說,有些人喜歡用Performance Counter去取得電腦的相關資訊,例如CPU的Loading及HD的IO,但有些資訊是Performance Counter並不提供的,這時,就可以透過WMI及WIN32取得更多的資訊,例如CPU的型號,Socket,外頻,電壓,溫度,BIOS..等. 當然部份資訊必需是硬體有這功能才能取得到的,像是溫度資訊,如果主機板沒有這個資訊提供,那就無法得到任何資訊了.


  講到這裡,WMI及WIN32到底有那些資訊可以提供?算起來真的很多,這裡就不一一的列示出來了,需要的人可以至MSDN內去看,找出那些資訊是你所需要用的到,以下就以取得CPU的資訊的程式碼範例.


using System.Management;

try
{
DataTable dt = New DataTable("TestData");
    dt.Columns.Add("DeviceID",typeof(string));
dt.Columns.Add("Name", typeof(string));
dt.Columns.Add("LoadPercentage", typeof(string));
dt.Columns.Add("MaxClockSpeed", typeof(string));
dt.Columns.Add("CurrentClockSpeed", typeof(string));
dt.Columns.Add("CurrentVoltage", typeof(string));
dt.Columns.Add("DataWidth", typeof(string));
dt.Columns.Add("ExtClock", typeof(string));
dt.Columns.Add("SocketDesignation", typeof(string));

ManagementObjectSearcher mos = new ManagementObjectSearcher(@"root\cimv2", "select * from Win32_Processor");
    //第一個參數填入這個Class的NameSpace,參考MSDN內的說明,就可以找到Win32_Processor的Namespace是什麼
    //第二個參數就跟SQL語法一樣,用Select的方法,取得Win32_Processor的資訊
foreach (ManagementObject mo in mos.Get())
{//在這裡,我是用Foreach的迴圈去取得CPU的資訊,因為CPU有可能是多核心的,所以要用迴圈去取得所有CPU的資訊
DataRow dr = tmpdt.NewRow();
dr["DeviceID"] = mo.Properties["DeviceID"].Value.ToString();//CPU的ID
dr["Name"] = mo.Properties["Name"].Value.ToString().Trim();//CPU的名稱,ex :Intel(R) Pentium(R) 4 CPU 2.60GHz
dr["LoadPercentage"] = mo.Properties["LoadPercentage"].Value.ToString();//Loading
dr["MaxClockSpeed"] = mo.Properties["MaxClockSpeed"].Value.ToString();//最大頻率
dr["CurrentClockSpeed"] = mo.Properties["CurrentClockSpeed"].Value.ToString();//目前頻率
dr["CurrentVoltage"] = mo.Properties["CurrentVoltage"].Value.ToString();//目前電壓
dr["DataWidth"] = mo.Properties["DataWidth"].Value.ToString();//位元
dr["ExtClock"] = mo.Properties["ExtClock"].Value.ToString();//外頻
dr["SocketDesignation"] = mo.Properties["SocketDesignation"].Value.ToString();//Socket描述 ex: Socket 478
dt.Rows.Add(dr);
}
    DataGrid1.DataSource=dt;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
 

  以上,就可以找出CPU的更詳細的資訊,其它像是BIOS,Share資源等資訊,同樣也是用此方式取得資訊,而各個Class可以取得什麼樣的資訊,於MSDN內都有詳細的說明. 以小弟的另一個自製程式ShareWatch,就是用此方式來找出電腦現在有那些共享資源,並有誰正在讀取這些資源,如果加上FileSystemWatch的功能,還可以知道使用者在異動那些檔案,或是電腦溫度過高時,發出Alter給相關管理維護人員,相信有了這些資訊後,可以做到相當多的應用,而怎麼用,就看你的想法囉.


 


參考資訊 : MSDN


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

日前為了解決公司系統效能的問題,真的是挖了不少解決方案出來,其中一個比較有意思的就是分頁處理.


分頁方式其實有非常的多種,例如把資料全部丟到Web,再分頁顯示,或是直接在DataAdapter.Fill時,下筆數,或是用Selete top 加上一些過濾及排序等動作.


但這些做法,都不能解決我現在的頻寬問題,因為分店連回總公司只有2M/256K的頻寬,如果一次倒回大量的資料,一定會打爆網路,而總公司的架構部份,WebService與DataBase也是不同台的伺服器,兩台伺服器也是需透過網路來傳輸,所以思考方向會是在如何在DataBase那,就做好分頁的動作. 不要有大量的資料在網路上傳輸.


為了這個問題,找了不少方法,像是Select top的方式,但因為程式查詢彈性的關係,造成這部份的邏輯相當的複雜,效能也不好. 最後找到SQL 2005才有的Row_number()這個Function可以用. 從一些參考網站所做的效能評比中,也算是數一數二的做法,所以這做法也是最後被公司採用的解決方案.


以下就是說明Row_number()的基本做法.


select a.UserName,a.UserID from (select row_number() over (order by UserID) as UID,UserName,UserID from UserDataInfo where bmi>20) as a where a.UID between @SP and @EP


這段語法就是關鍵所在,正常來說,我的語法只有紅字部份,帶出所有bmi>20的人員,一次把資料全部回傳,但這裡卻多了一段藍字在裡面,Row_Number()就是將符合條件的結果,再從1開始,依序給予編號,所以回傳的結果就會是


UID     UserName     UserID


1          Jeff                   A0001


2          Jerry              C0014


3          Judy               C0096


4          Mark               D0002


5          Jason               D0010


6          Rober               D0011


7          Martin            D0022


所以黑色部份的語法,就依這個的查詢結果,取出UID介於幾號到幾號之間. 所以就可以在DataAccess端,依據設定的PageSize,來算頁次,再依目前的頁次與PageSize去帶回所需顯示的資料範圍.


如果不分頁的話,一次載入上萬筆資料是很可怕的,尤其是透過2M/256K的頻寬,這是ERP系統,不是P2P下載平台,使用者不可能等,如果用分頁方式,就只會一次傳回指定的筆數,大幅的減少網路的負擔,而且這是在DB就做好的動作,所以從DataBase到WebService這段的網路問題也解決了.


這做法也不是沒有缺點,例如在換頁過程中,突然又有幾筆資料符合或不符合了,其順序就會異動,就有可能剛好在換頁時,某筆資料會沒看到,可能跳到前頁或下頁去了. 這部份就看各家使用者了.


另一個問題就是筆數一多時,頁次愈換到後面,速度會愈慢,但這也是百萬筆資料的事,當有這麼多資料時,應該也沒有什麼人會一頁一頁的去看完它.


Row_number()更多的說明,參考MSDN 或是GOOGLE大神

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

最近在寫一只程式,當動作完成時,會寄信通知.


而這程式原本是從VS2003升級到VS2005的,有些語法過時的,就轉到VS2005的物件.


其中有一個物件在修改後,卻發現一個奇怪的地方.


信會寄不出來?而我在測試時,並沒有問題啊.


檢查了一下資料庫的郵件位址的資料.


發現存進去的資料格式不是只有單純的郵件位置,其格式如下:


使用者名稱<test@test.com>


如果有多郵件地址時,則是用分號;的方式來串接


使用者名稱1<test1@test.com

>;使用者名稱2<test2@test.com>

但這種格式在System.Web.Mail.MailMessage的方式寄信,並不會有問題.


改用System.Net.Mail.MailMessage的2.0新增的元件寄信,就會有問題產生了.


寫了一個小程式試了一下信件格式的問題.


使用MailMessage.To.Add("使用者名稱<test@test.com>"); 失敗,無法寄,會跳出"指定字串不在電子郵件地址的必要表單中"的錯誤訊息.


但如果是MailMessage.To.Add("Test<test@test.com>");則是可以順利寄出.


所以如果顯示名稱要用中文的話,就必需改一點寫法


MailMessage.To.Add(new MailAddress(("使用者名稱<test@test.com>"));


當然MailAddress可選其它的方法,把顯示名稱放到第二個參數內,但這做法就要修改整個資料庫的資料,或是改用資料截取的方式,如果不行,情願用System.Web.Mail來寫.


再來就是多收件者用分號;串接的方式.


System.Net.Mail也不支援,同樣會跳出"指定字串不在電子郵件地址的必要表單中"的錯誤訊息.


必需一個一個用MailMessage.To.Add的方式加進去.


所以整個修改後,大致上的程式碼如下 :


 try
{
    string[] toa = textBox1.Text.Trim().Split(";".ToCharArray());
    MailMessage newMail = new MailMessage();
    newMail .From = new MailAddress("訊息通知<info@XXX.com.tw">");
    newMail .Priority = MailPriority.Low;
    newMail .IsBodyHtml = true;
    newMail .Body = "TEST";
    foreach (string to in toa)
    {
        myMail.To.Add(new MailAddress(to));
     }
     newMail .Subject = "Test";


     SmtpClient sc = new SmtpClient("ext2.genuine.com.tw");
     sc.Send(newMail );
}
catch (Exception ex)
{
     MessageBox.Show(ex.Message);
}


還好,最後還是能寄了,不用改資料庫的資料,如果真的要改,那就改回去用System.Web.Mail~

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

最近在用Visual studio 2002 C#在開發WinForm程式時,遇到一個訊息有點讓人不解. 在Debug時,中斷點並沒有發揮作用.程式不會在中斷點停住,而在中斷點的那個紅點中,可以看到一個?號,滑鼠移過去後,就看到這段訊息 : The breakpoint will not currently be hit. No symbols have been loaded for this document


無法設中斷點去Debug,真的很難去Trace一些值,所以就開始一一的去確認,到底是為什麼中斷點沒作用.


首先,確認現在是Debug Mode,還是Release Mode. (曾忘了改,結果在Release Mode執行,想著為什麼不能除錯),結果確認完是在Debug Mode沒錯.


再來檢查pdb檔是否有更新,exe或dll,其時間必需與pdb相同才對. 這時發現,怎麼pdb沒有更新.


因為我的solution內有兩個專案,一個winform,另一個是class,而winform有把class加入參考,中斷點失效及pdb沒更新的,就是那個class,而winform的pda卻有正常更新.


想說會不會是加入參考有問題還是什麼的,就移掉,重新加入參考,結果還是沒用.


這時就一頭霧水了,為什麼就只有它的pdb沒有更新.


這時突然想到,這個Solution有加入到SourceSafe,會不會也有影響,就去檢查了一下SourceSafe的設定.


結果,bin被加入到SourceSafe去,連class的pdb也加進去了,難怪我在Debug時,class的pdb都沒有更新.


把bin整個從SourceSafe移除後,重新執行Debug,中斷點就生效了.


 


這結果有點讓自己哭笑不得,有點自己在耍白痴.


但也好奇,有多少人有這個問題,用google查了一下,還真是不少人有這個問題.


而且不是只有VS2002,連2003/2005都會發生.


原來我並不孤單~~~~

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

用了一段時間的OWC來製圖,它是還不錯用,但現在發現了另一個選擇,而且也是免費的元件(目前是),可用在Web Form及Windows Form上.


使用難度並不難,以下是用Win Form的方式來說明.


第一步


當然要先去下載這個物件. 由於資料來源是CodeProject,所以就不自己再放另一個下載點,請至CodeProject下載. 視各別需要,否則只要下載dll only的即可.


第二步


把下載回來的壓縮檔,解壓縮至你想要存放的目錄.


第三步


使用VS2005開啟一個新的Windows Application(也可用VS2003,只是此範例用VS2005).


第四步


在工具箱的地方,按右鍵,選擇項目,在.NET Framework元件的頁籤,按下瀏覽,到剛剛解壓縮的那個目錄下.


於解壓縮的目錄下,找一下ZedGraph.dll,這個有兩個版本.


4.5+是適用於.Net Framework 1.1


5.0+是適用於.Net Framework 2.0


註:ZedGraph.Web.dll就是適用於WebForm


第五步


加入後,在工具箱就會看到ZedGraphControl,就可以開始進行介面的設計,於介面上,放兩個RadioButton,各自命名為rdb_Bar及rdb_Line,rdb_Bar為長條圖,rdb_Line為折線圖. 再加入一個ZedGraphControl.如下圖:



第六步


到此,已完成一半的工作了,接下來,加一個Form1_Load的事件,及一個RadioButton_CheckedChanged的事件,此事件由兩個RadioButton共用.


在這兩個事件內,程式碼如下:


        private void Form1_Load(object sender, EventArgs e)
        {
            zedGraphControl1.GraphPane.Title.Text = "銷售統計"; //圖表的表頭
            zedGraphControl1.GraphPane.XAxis.Title.Text = "時間"; //X軸的名稱
            zedGraphControl1.GraphPane.YAxis.Title.Text = "金額"; //Y軸的名稱
            ShowChart();
        }


        private void rdb_CheckedChanged(object sender, EventArgs e)
        {
            ShowChart();
        }


而ShowChar()就是顯示統計圖的主要地方.


private void ShowChart()
        {
            GraphPane myPane = zedGraphControl1.GraphPane;
            zedGraphControl1.GraphPane.CurveList.Clear();//把舊的圖資清掉


            string[] xTitle ={ "2007/8", "2007/9", "2007/10", "2007/11" }; //X軸的刻度資料
            double[] PC ={ 5000000, 5800000, 6300000, 7000000 };//PC商品的金額
            double[] NB ={ 5100000, 5400000, 6900000, 7500000 };//NB商品的金額


            BarItem myBar;
            LineItem myCurve;


            #region 長條圖
            if (rdb_Bar.Checked)
            {
                myBar = myPane.AddBar("PC銷售額", null, PC, Color.Red);
                myBar.Bar.Fill = new Fill(Color.Red, Color.White, Color.Red);


                myBar = myPane.AddBar("NB銷售額", null, NB, Color.Purple);
                myBar.Bar.Fill = new Fill(Color.Purple, Color.White, Color.Purple);
            }
            #endregion


            #region 折線圖
            if (rdb_Line.Checked)
            {
                myCurve = myPane.AddCurve("PC銷售額", null, PC, Color.Red);
                myCurve.Line.Fill = new Fill(Color.Red, Color.White, Color.Red);


                myCurve = myPane.AddCurve("NB銷售額", null, NB, Color.Purple);
                myCurve.Line.Fill = new Fill(Color.Purple, Color.White, Color.Purple);
            }
            #endregion


            myPane.XAxis.Scale.TextLabels = xTitle;
            myPane.XAxis.Type = AxisType.Text;
            myPane.Chart.Fill = new Fill(Color.White, Color.FromArgb(255, 255, 166), 90F);
            myPane.Fill = new Fill(Color.FromArgb(250, 250, 255));
            zedGraphControl1.AxisChange();
            zedGraphControl1.Refresh();
        }


到此,程式就完成了,執行後的畫面就像下面的圖示


長條圖:



折線圖



在圖上按下右鍵,也有一些不錯的內建功能



 


這只是一個簡單的範例,這元件的功能還很多,在CodeProject的圖例就不少了.有興趣的人,可以玩看看.


原始碼 : C#下載   VB下載


使用DataSource範例的原始碼 : C#下載

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

很多人應該都有使用過Performance效能監視器的這個功能,有去細玩過的人,都會發現到它的好用,但說好用,似乎又少了那麼一丁點的功能,總是有那麼一點遺憾,Visual Studio內有Performance Counter這個元件可以供使用,所以接下來就可以用這個物件,來寫出我們所需要的功能.


 


這個Performance Counter元件在Visual Studio 2002/2003/2005內都有,並不是什麼新的功能,以下的例子,是採用VS 2005 C#所撰寫的.


 


現在要舉的這個使用範例,可能是部份MIS人員所會遇到的,當有一個自己開發的程式放在某台伺服器上,但我們又不可能隨時登入去看那個程式是否還在正常執行,如何籍由另一個程式來監控它,並回報檢查結果,讓我們能隨時掌握它的狀況,也不需一直登入去看.上班就喝咖啡,聊是非就好~


 


有一點要注意的,就是這個只支援.Net的程式,以下為程式範例 :


 


public string CheckAP(string MachineName,string InstanceName)


{//MachineName電腦名稱,InstanceName程式名稱


    PerformanceCounter pc = new PerformanceCounter();


    try


    {


        pc.CategoryName = ".NET CLR Exceptions";//這裡所設定的就是效能監視器的效能物件,看到這個,就知道為什麼這程式只支援.Net的程式


        pc.CounterName = "# of Exceps Thrown";//這裡所設定就是效能監視器的計數器


        pc.ReadOnly = true;


        pc.InstanceLifetime = PerformanceCounterInstanceLifetime.Global;


        pc.MachineName = MachineName.Trim();


        pc.InstanceName = InstanceName.Trim();


        return pc.NextValue().ToString();//回傳發生Exception的次數


    }


    catch (InvalidOperationException ioe)


    {


        if (Process.GetProcessesByName(InstanceName, MachineName).Length == 0)//檢查程式是否沒有執行


        {


            return " 程式未執行!";


    }


        else


    {


            return ioe.Message;


        }


}


catch (Exception ex)


{


        return ex.Message;


}


finally


{


        pc.Close();


    pc.Dispose();


    PerformanceCounter.CloseSharedResources();//釋放由計數器配置的效能計數器程式庫的共用狀態


}


}


 


之後,只要用Timer定時的去呼叫這一段程式,就可以知道監控中的那個程式,是否運作正常.如果異常,再依個人的需求去做處理就好,這個例子,是用來抓Exception,但效能監視器可不是只能監控這個,它還可以監控記憶體,IO,CPU,SQL,網路,所以這個就可以有很大的發揮空間,看大家怎麼去利用Performance Counter所回傳的結果去做處理囉.


 


詳細說明,請參考Visual StudioHelp,Performance Counter.

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

        以下是使用Visual Studio 2005 C #所寫的FTP 上傳/下載及取得清單的程式範例,而這個的.Net Framework需為2.0或以上.


 


        首先,大至上講解一下,在使用FTP,所以使用到的幾個FTP物件FtpWebRequest,FtpWebResponse,WebRequestMethods. 這四個為FTP,較為重要的幾個(當然範例中也有用到Stream,FileStream,StreamReader,NetworkCredential, WebRequest但這幾個並非是FTP才會用到的,所以不做特別介紹,如果需要瞭解,可點取連結至MSDN參考說明).其中以FtpWebRequest, FtpWebResponse, WebRequestMethods這三個物件為.Net 2.0才有的,1.0,1.1並沒有.


 


        FtpWebRequest主要就能與FTP伺服器產生互動的功能,例如設定Timeout逾時設定,KeepAlive保持連線,EnableSsl設定是否使用SSL連線,Credential通訊協定認證,GetResponses取得伺服器回應,GetRequestStream用來上載至伺服器的資料流等,看到這個地方,大概就能知道,這一個物件是用來上傳使用的,並且設定其通訊協定.


 


        而另一個物件FtpWebResponse,主要是用來取得伺服器對要求的回應,像是StatusCode取得FTP伺服器回傳最新的狀態碼,WelcomeMessage驗證完成後,所取得到的文字,LastModified取得FTP伺服器上的檔案最近的修改日期及時間,GetResponseStream擷取FTP伺服器回應的資料流(簡單的說,就是下載).


 


        WebRequestMethods,用來設定通訊協定的命令,這樣看起來似乎很難懂,當我們在宣告FtpWebRequest,就需要設定Method,舉例來說:


FtpWebRequest listRequest = (FtpWebRequest)WebRequest.Create(listUrl);


listRequest.Method = WebRequestMethods.Ftp.ListDirectoryDetails; //用來取得目錄的檔案清單


listRequest.Method = WebRequestMethods.Ftp. UploadFile;//上傳檔案


listRequest.Method = WebRequestMethods.Ftp. DownloadFile;//上傳檔案


由此大概就可以知道,它是在設定FtpWebRequest傳送至FTP伺服器的命令.當然也包含DeleteFile刪檔,Rename更名.


 


        以上三個FtpWebRequest,FtpWebResponse,WebRequestMethods的更多詳細用法,請參考Visual Studio 2005Help.


 


        接下來就要帶到,如何用C#寫出下載,上傳及取得清單的方法 :


1.      下載程式範例


internal bool Download(string downloadUrl,string TargetPath, string UserName, string Password)


{//downloadUrl下載FTP的目錄ex : ftp//127.0.0.1/abc.xml , TargetPath本機存檔目錄,UserName使用者FTP登入帳號 , Password使用者登入密碼


Stream responseStream = null;


    FileStream fileStream = null;


    StreamReader reader = null;


    try


    {


        FtpWebRequest downloadRequest = (FtpWebRequest)WebRequest.Create(downloadUrl);


        downloadRequest.Method = WebRequestMethods.Ftp.DownloadFile; //設定Method下載檔案


        if (UserName.Length > 0)//如果需要帳號登入


        {


            NetworkCredential nc = new NetworkCredential(UserName, Password);


            downloadRequest.Credentials = nc;//設定帳號


        }


        FtpWebResponse downloadResponse = (FtpWebResponse)downloadRequest.GetResponse();


        responseStream = downloadResponse.GetResponseStream();//取得FTP伺服器回傳的資料流


        string fileName = Path.GetFileName(downloadRequest.RequestUri.AbsolutePath);


        if (fileName.Length == 0)


        {


            reader = new StreamReader(responseStream);


            throw new Exception(reader.ReadToEnd());


        }


        else


        {


            fileStream = File.Create(TargetPath+@"\"+fileName);


            byte[] buffer = new byte[1024];


            int bytesRead;


            while (true)


            {//開始將資料流寫入到本機


                bytesRead = responseStream.Read(buffer, 0, buffer.Length);


                if (bytesRead == 0)


                break;


                fileStream.Write(buffer, 0, bytesRead);


            }


        }


        return true;


    }


    catch (IOException ex)


    {


        throw new Exception(ex.Message);


    }


    finally


    {


        if (reader != null)


        reader.Close();


        else if (responseStream != null)


        responseStream.Close();


        if (fileStream != null)


        fileStream.Close();


    }


}


 


2.      上傳程式範例


internal bool Upload(string fileName, string uploadUrl,string UserName,string Password)


{//fileName上傳的檔案ex : c:\abc.xml , uploadUrl上傳的FTP伺服器路徑ftp://127.0.0.1,UserName使用者FTP登入帳號 , Password使用者登入密碼


Stream requestStream = null;


    FileStream fileStream = null;


    FtpWebResponse uploadResponse = null;


    try


    {


        FtpWebRequest uploadRequest = (FtpWebRequest)WebRequest.Create(uploadUrl);


        uploadRequest.Method = WebRequestMethods.Ftp.UploadFile;//設定Method上傳檔案


        uploadRequest.Proxy = null;


        if (UserName.Length > 0)//如果需要帳號登入


        {


            NetworkCredential nc = new NetworkCredential(UserName, Password);


            uploadRequest.Credentials = nc; //設定帳號


        }


        requestStream = uploadRequest.GetRequestStream();


        fileStream = File.Open(fileName, FileMode.Open);


        byte[] buffer = new byte[1024];


        int bytesRead;


        while (true)


        {//開始上傳資料流


            bytesRead = fileStream.Read(buffer, 0, buffer.Length);


            if (bytesRead == 0)


            break;


            requestStream.Write(buffer, 0, bytesRead);


        }


        requestStream.Close();


        uploadResponse = (FtpWebResponse)uploadRequest.GetResponse();


        return true;


    }


    catch (Exception ex)


    {


        throw new Exception(ex.Message);


    }


    finally


    {


        if (uploadResponse != null)


        uploadResponse.Close();


        if (fileStream != null)


        fileStream.Close();


        if (requestStream != null)


        requestStream.Close();


    }


}


 


3.      取得清單程式範例


internal Hashtable List(string listUrl,string UserName,string Password)


{//listUrl FTP伺服器路徑ftp://127.0.0.1,UserName使用者FTP登入帳號 , Password使用者登入密碼


StreamReader reader = null;


    try


    {


        FtpWebRequest listRequest = (FtpWebRequest)WebRequest.Create(listUrl);


        if (UserName.Length > 0)//如果需要帳號登入


        {


            NetworkCredential nc = new NetworkCredential(UserName, Password);


            listRequest.Credentials = nc; //設定帳號


        }


        listRequest.Method = WebRequestMethods.Ftp.ListDirectoryDetails;// //設定Method取得目錄資訊


        FtpWebResponse listResponse = (FtpWebResponse)listRequest.GetResponse();


        reader = new StreamReader(listResponse.GetResponseStream());


        Hashtable ht = new Hashtable();


        int i=0;


        while (reader.Peek()>=0)


        {//開始讀取回傳資訊


            ht[i] = reader.ReadLine();


            i++;


        }


        return ht;//回傳目前清單


               


    }


    catch (Exception ex)


    {


        throw new Exception(ex.Message);


    }


    finally


    {


        if (reader != null)


        reader.Close();


    }


}


 


        上面三個程式範例為基本的程式範例,當中當然還可以加入一些判斷,例如最常出錯的就是url,這點要注意的是,URL的最後面不需跟著[ / ],正確的URL : ftp://127.0.0.1 , 錯誤的URL : ftp://127.0.0.1/ ,所以在URL的部份,可以加上這個的判斷,減少因為使用者誤Key,而導致的錯誤結果.


 


以上說明或範例如有錯誤,歡迎指正~

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

«12 3