2013年2月7日 星期四

.Net and SAP Integration: shared function RFC_READ_TABLE

  • 前言:RFC_READ_TABLE 是 SAP 提供的 Read SAP Table 的公用程式,對於需要取得SAP資料的.Net Programmer而言,經常會使用到這個程式,因此適合開發成共用層。這一篇則是將RFC_READ_TABLE 包裝成 WCF 提供服務,SAP端則是以 ECC6 Unicode 版本為目的地。對於本篇的內容,歡迎建議與討論!
  • 使用工具:
    • VS 2010、.Net Framework 4.0
    • SAP NCO 3.0 for .Net Framework 4.0:請自行到SAP網站下載,建議取得最新版,SAP更新的滿快的,舊版曾經發生一些奇怪的問題,安裝好的目錄通常在:C:\Program Files (x86)\SAP 底下,會需要 sapnco.dll 與 sapnco_utils.dll。另外,位元版本也需要注意:
      • Winform: 與專案版本相同,如果專案是64bit,則使用64bit
      • Web application、Web Service、WCF: 都使用32bit
    • 黃昭仁大大的"IRfcTable、DataTable、DataSet 資料轉換模組"(http://vsqa.blogspot.tw/2011/08/irfctabledatatabledataset.html):轉換 NCO 回傳的Table資料結構轉換成.Net Programmer適合使用的 Data Table, DataSet,非常好用。
    • SAP RFC_READ_TABLE:要用的好,必須先對這個function 的用法,詳細可以開 SAP GUI 先試試看。建議在開發前,先在SAP確認好資料都正確,SAP RFC_READ_TABLE 正確,在外部呼叫一定都會正確。
  • WCF 運作方式:WCF主要就是提供 RFC_READ_TABLE去設計,服務的input跟SAP相同,只是我沒有放RowCount,個人認為是不需要,各位看官如果覺得有需要,請再加上去就好。
    • 服務的宣告:
      [OperationContract]
      SAPMessageType SAP_RFC_READ_TABLE_UNICODE(string strDest, string strTable, string[] strOptions, string[] strFields);

      參數說明如下:
      > strDest:SAP目的地
      > strTable: 要查詢的SAP Table
      > strOptions: SAP Options 也就是 Where 的條件,每一行有長度限制是72
      > strFields: SAP Fileds 要查詢SAP Table哪一些欄位
      其中,回傳的資料結構(合約)我自行定義的,因為通常Export會回傳執行的狀態,而 Table 則存放實際的資料,所以把這些合成一種資料結構
      [DataContract]
      public class SAPMessageType
      {
          [DataMember]
          public DataTable SAPTable { get; set; }
          [DataMember]
          public string strMessage { get; set; }
          [DataMember]
          public string strMessageCode { get; set; }
      }
      
    • WCF 執行過程:邏輯上與資料庫連結是一樣的,主要分5區,
      • 第一區 宣告區:主要是宣告會使用到的變數等等,小弟比較老一點,所以會先宣告要用到的變數
      • 第二區 連線區:從連線的class取得連線參數,並開始連線
      • 第三區 傳入參數區:指定SAP Function(RFC_READ_TABLE),傳入Fileds, Options
      • 第四區 回傳參數與處理:取得回傳的SAP 資料,並根據Fields所指定的開始切割
      • 第五區 結束區
        [PrincipalPermission(SecurityAction.Demand, Authenticated = true)]
        public SAPMessageType SAP_RFC_READ_TABLE_UNICODE(string strDest, string strTable, string strOptions, string[] strFields)
        {
            //
            //第一區 宣告區: logger 是使用nlog
            //
            logger.Info("SAP_RFC_READ_TABLE START UNICODE Version Start");
            SAPMessageType aData = new SAPMessageType();
            string strMessage = "";
            string strMessageCode = "";

            DataTable SAPDataTable = null;
            RfcDestination destination = null;

            try
            {
                //
                //第二區 連線區,連線內容在 SAPSystemConnect.cs 中取出
                //
                SAPSystemConnect aSapCfg = new SAPSystemConnect();
                RfcConfigParameters parameters = aSapCfg.GetParameters(strDest);

                if (parameters == null)
                {
                    //指定的SAP Client不存在
                    strMessageCode = "E";
                    strMessage = "指定的SAP Client不存在";
                }
                else
                {
                    destination = RfcDestinationManager.GetDestination(parameters);
                    RfcSessionManager.BeginContext(destination);

                    destination.Ping();
                    IRfcFunction function = null;

                    //
                    //第三區 傳入參數區
                    //

                    function = destination.Repository.CreateFunction("RFC_READ_TABLE");
                    function.SetValue("QUERY_TABLE", strTable);

                    //OPTIONS
                    if (strOptions != "")
                    {
                        IRfcTable tableOPTIONS = function["OPTIONS"].GetTable();
                        tableOPTIONS.Append();
                        tableOPTIONS.SetValue(0, strOptions);
                    }

                    IRfcTable tableFIELDS = function["FIELDS"].GetTable();
                    if (strFields.Length != 0)
                    {
                        foreach (string fd in strFields)
                        {
                            tableFIELDS.Append();
                            tableFIELDS.SetValue("FIELDNAME", fd);
                        }
                    }

                    function.Invoke(destination);

                    //
                    //第四區 回傳參數與處理
                    //

                    IRfcTable tableRead = function.GetTable("DATA");
                    DataTable dtRealTable = new DataTable(strTable);
                    ArrayList listFields = new ArrayList();
                    foreach (IRfcStructure row in tableFIELDS)
                    {
                        RFC_Table_Schema aRowSchema = new RFC_Table_Schema();
                        aRowSchema.fdOffset = Convert.ToInt16(row.GetString("OFFSET"));
                        aRowSchema.fdLength = Convert.ToInt16(row.GetString("LENGTH"));
                        aRowSchema.fdName = row.GetString("FIELDNAME");
                        aRowSchema.fdType = row.GetString("Type");
                        listFields.Add(aRowSchema);

                        //準備存放整理後資料的DataTable
                        dtRealTable.Columns.Add(row.GetString("FIELDNAME"), typeof(String));
                    }

                    foreach (IRfcStructure row in tableRead)
                    {
                        DataRow realRow = dtRealTable.NewRow();
                        //根據實際資料切Table內容, WA是為分割前的資料
                        String strWA = row.GetString("WA");

                        //unicode
                        #region Unicode切割方式

                        string anewString;

                        foreach (RFC_Table_Schema aRowSchema in listFields)
                        {
                            anewString = "";
                            //根據長度切割
                            //如果自SAP取得資訊,發現後面都是空白,SAP就會自己Trim字串,資料會與Fields提供的資訊不同
                            if ((aRowSchema.fdOffset + aRowSchema.fdLength) > strWA.Length)
                            {
                                if ((strWA.Length - aRowSchema.fdOffset) > 0)
                                {
                                    anewString = strWA.Substring(aRowSchema.fdOffset, (strWA.Length - aRowSchema.fdOffset));
                                }
                            }
                            else
                            {
                                anewString = strWA.Substring(aRowSchema.fdOffset, aRowSchema.fdLength);
                            }

                            realRow[aRowSchema.fdName] = anewString.Replace("\0", "").Trim();
                        }
                        #endregion

                        dtRealTable.Rows.Add(realRow);

                    }

                    //
                    //第五區 結束區
                    //

                    SAPDataTable = dtRealTable;
                    strMessageCode = "S";
                    strMessage = "成功讀取";

                    RfcSessionManager.EndContext(destination);
                    destination = null;
                }

            }
            catch (Exception ex)
            {
                logger.Error(ex.ToString());

                strMessageCode = "E";
                strMessage = ex.ToString();
                if (destination != null)
                {
                    RfcSessionManager.EndContext(destination);
                    destination = null;
                }
            }

            aData.SAPTable = SAPDataTable;
            aData.strMessage = strMessage;
            aData.strMessageCode = strMessageCode;
            logger.Info("SAP_RFC_READ_TABLE START UNICODE Version END");
            return aData;
        }



  • Service 使用端:呼叫時,先引用WCF,依照這樣呼叫:
  • //使用WCF
    SAPServiceReference.SAPServiceClient aClient = new SAPServiceReference.SAPServiceClient();
    SAPServiceReference.SAPMessageType aClientData = null;
    
    //需要的欄位
    string[] strFds2 = new string[] { "BUKRS", "BUTXT", "LAND1" };
    //Where條件
    string[] strOps = new string[] { "LAND1 <> 'TW'", "AND MANDT = '500' " };
    //取得 T001 公司主檔
    aClientData = aClient.SAP_RFC_READ_TABLE_UNICODE("DEV500", "T001", strOps, strFds2);
    aClient.Close();
    
    
    //因為回傳 Datatable,所以可以直接用
    GridView4.DataSource = aClientData.SAPTable;
    GridView4.DataBind();
    //可以有一個Label說明執行的結果
    lbNewErr.Text = aClientData.strMessageCode + ":" + aClientData.strMessage;
    

如果有需要程式參考的大大,請到這邊:

  • https://dl.dropbox.com/u/3330791/Sharecode/SAPWcfService.rar 
    • 程式目前會無法執行,因為要請自行下載SAP NCO 3.0,並加入 sapnco.dll、sapnco_utils.dll

參考資料:

  • SAP NCO 手冊: http://help.sap.com/saphelp_crm700_ehp02/helpdata/EN/4a/097b0543f4088ce10000000a421937/content.htm 
  • 黃昭仁大大的 Blog: http://vsqa.blogspot.tw/ 
  • How-To Use SAP Nco 3 Connector | .Net 4 | Visual Studio 2010: http://klanguedoc.hubpages.com/hub/How-To-Use-SAP-Nco-3-Net-4-Visual-Studio-2010

#異質資料介接

2012年11月26日 星期一

Develop Google Maps API v3 Map Pages with ASP.NET AJAX


  • 前言:Google Maps API 提供服務以來,由於容易開發、API功能多更新快並且又穩定,只要不踩到一些地雷,就不會收錢,所以一直是地圖開發者喜歡用的工具API。這一篇是要介紹,從資料庫取得點位資料套疊到地圖上。我使用的工具是 ASP.Net 使用 Google Map API v3 ,並且用AJAX,是因為如果用標準的asp.net button,會submit/postback 回 server,如果使用者在submit之前就做了一些地圖操作(放大、移動..),就會因為submit to server 會回到初始畫面,這樣的操作模式是滿令人不悅的,所以才需要AJAX方式來處理使用者對地圖的操作。
  • 網頁功能:取得使用者目前座標,繪製地點與精確度的範圍。並根據使用者點選的圖層類型,帶入對應的點位。
  • 瀏覽器限制:因為有用到HTML5,IE8,9確定不行,用 Chrome Ok, IPhone Safari OK
  • 使用元件:
    • Google Map API V3 :必要
    • Visual Studio 2012:必要
    • JQuery mobile:非必要,放著只為了可以在行動裝置上看。
    • HTML5:必要,為了取得目前座標。所以..那個 IE
  • 網頁主要分成 3 區 
    • (1)地圖區:顯示地圖
    • (2) ASP.Net button 區:負責從資料庫取得資料,並呈現在地圖
    • (3) JS Button區:主要是 javascript button,是操作google map的功能,如"取得目前位置"、"清除地圖" 這類的功能

  • 以下列出比較重要的程式片段,完整的程式請到網頁最下方再去下載。
  • HTML Head區:需要注意的是 style 那一區,因為 Google Map DIV必須指定大小,所以指定在Head裡面。其他就是引用 jquery 與 google map api v3
<head>
    <title>Google Maps API v3 and ASP.Net AJAX</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1" /> 
    <style type="text/css">
      html { height: 100% }
      body { height: 100%; margin: 0px; padding: 0px }
      #map_canvas { height: 100% }
    </style>
 <link rel="stylesheet" href="/css/jquery.mobile-1.2.0.min.css" />
    <script src="/js/jquery-1.8.2.min.js"></script>
    <script src="/js/jquery.mobile-1.2.0.min.js"></script>
    <script src="https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false"></script>

  • 再下來就是控制google maps div 的區域,首先是global的變數
        //Global variables
        var map;            //地圖
        var marker;         //地圖上的點
        var infowindow;     //點上面跳出的視窗
        var overlays = [];  //目前地圖上所有套疊的圖層
        var mapcenter = new google.maps.LatLng(25.228664, 121.750202);  //預設的地圖中心

  • 地圖初始化的function,等下會在body onload呼叫。裡面的詳細API 請參考Google 文件。在初始化完成後,呼叫 取得目前座標的function: getLocation。
        function initialize() {
            var mapOptions = {
                zoom: 6,
                mapTypeId: google.maps.MapTypeId.ROADMAP,
                center: mapcenter,
                mapTypeControl: true,
                mapTypeControlOptions:
                {
                    style: google.maps.MapTypeControlStyle.DROPDOWN_MENU,
                    poistion: google.maps.ControlPosition.TOP_RIGHT,
                    mapTypeIds: [google.maps.MapTypeId.ROADMAP,
                    google.maps.MapTypeId.TERRAIN,
                    google.maps.MapTypeId.HYBRID,
                    google.maps.MapTypeId.SATELLITE]
                },
                navigationControl: true,
                navigationControlOptions:
                {
                    style: google.maps.NavigationControlStyle.ZOOM_PAN
                },
                scaleControl: true,
                disableDoubleClickZoom: false,
                streetViewControl: true,
                draggableCursor: 'move'
            };

            infowindow = null;
            infowindow = new google.maps.InfoWindow({
                content: "info window content"
            });

            map = new google.maps.Map(document.getElementById('map_canvas'),
            mapOptions);
            
            //取得使用者目前座標
            getLocation();
        }

  • 取得目前位置化的function:使用HTML5的方式取得座標(點)與精確度(圓圈),並繪製到地圖上。
        //
        //取得目前位置
        //
        function getLocation() {
            var x = document.getElementById("Message");
            if (navigator.geolocation) {
                navigator.geolocation.getCurrentPosition(showPosition);
                //如果要持續取得,用watchPosition
//navigator.geolocation.watchPosition(showPosition); } else { x.innerHTML = "Geolocation is not supported by this browser."; } } function showPosition(position) { var x = document.getElementById("Message"); //x.innerHTML = "Latitude: " + position.coords.latitude + " Longitude: " + position.coords.longitude + " Accuracy" + position.coords.accuracy; var newMapCenter = new google.maps.LatLng(position.coords.latitude, position.coords.longitude); map.setCenter(newMapCenter); //以marker方式出現 setMarker(newMapCenter, "My Current Location" + " Accuracy is :" + position.coords.accuracy); //以圓形出現 if (position.coords.accuracy != null) { DrawCircle(newMapCenter, position.coords.accuracy); if (position.coords.accuracy < 1000) { map.setZoom(14); } } else { DrawCircle(newMapCenter, 100); } }
  • 繪圖用的function,繪製圓形、繪製單一點座標與視窗(infowindow)訊息
        //繪製圓形
        function DrawCircle(center,rad) {
            var draw_circle;
            draw_circle = new google.maps.Circle({
                center: center,
                radius: rad,
                strokeColor: "#FF0000",
                strokeOpacity: 0.8,
                strokeWeight: 2,
                fillColor: "#FFFF00",
                fillOpacity: 0.35,
                map: map
            });
            overlays.push(draw_circle);
        }

        //繪製單一點座標
        function setMarker(singleCoord,infoWindowContent) {
            marker = new google.maps.Marker({
                map: map,
                draggable: true,
                animation: google.maps.Animation.DROP,
                position: singleCoord
            });

            google.maps.event.addListener(marker, 'click', function () {
                infowindow.setContent(infoWindowContent);
                infowindow.open(map, this);
            });

            //
            overlays.push(marker);
        }

        //產生彈跳效果
        function toggleBounce() {
            if (marker.getAnimation() != null) {
                marker.setAnimation(null);
            } else {
                marker.setAnimation(google.maps.Animation.BOUNCE);
            }
        }
  • 繪製多個點,提供給asp.net程式呼叫。Javascript Array的格式是['Mount Evans', 59.32522, 18.17002, 4, 'This is Mount Evans.'];,C#要繪製多點的時候,就是去準備出這個多筆陣列,再交給 setMarkers 去繪製。
        function setMarkers(markers) {
            for (var i = 0; i < markers.length; i++) {
                var sites = markers[i];
                var siteLatLng = new google.maps.LatLng(sites[1], sites[2]);
                var marker = new google.maps.Marker({
                    position: siteLatLng,
                    map: map,
                    title: sites[0],
                    zIndex: sites[3],
                    html: sites[4]
                });

                var contentString = "info window content";

                google.maps.event.addListener(marker, "click", function () {
                    infowindow.setContent(this.html);
                    infowindow.open(map, this);

                    if (marker.getAnimation() != null) {
                        marker.setAnimation(null);
                    } else {
                        marker.setAnimation(google.maps.Animation.BOUNCE);
                    }
                });

                //
                overlays.push(marker);
            }
        }
  • 清除全部繪製的圖層
        function clearMarkers() {
            while (overlays[0]) {
                overlays.pop().setMap(null);
            }
        }
  • 另外在.ASPX網頁設計上,只有ASPX的Controls(Button1~4,顯示A~D類的圖層資料),放在UpdatedPanel裡面。Google Map DIV 更要在Form 以外才能正常顯示。控制地圖的HTML Button(Button 6,7)則是單純的HTML button就可以
<div id="Message"></div>
    <div id="map_canvas" style="width:100%; height:80%"></div>
    <form id="form1" runat="server">
        <asp:ScriptManager ID="ScriptManager1" runat="server">
        </asp:ScriptManager>
        <asp:UpdatePanel ID="UpdatePanel1" runat="server">
            <ContentTemplate>
                AJAX Button:<asp:Button ID="Button1" runat="server" OnClick="Button1_Click" Text="All" />
                <asp:Button ID="Button2" runat="server" OnClick="Button2_Click" Text="A" />
                <asp:Button ID="Button3" runat="server" OnClick="Button3_Click" Text="B" />
                <asp:Button ID="Button4" runat="server" OnClick="Button4_Click" Text="C" />
                <asp:Label ID="Label1" runat="server" Visible="False"></asp:Label>
                <br />
                <asp:Label ID="lbErr" runat="server" Visible="False"></asp:Label>
                <br />
            </ContentTemplate>
        </asp:UpdatePanel>
        JS Button:
        <input id="Button5" type="button" value="Find My Location" onclick="getLocation()"/><input id="Button6" type="button" value="Clear Markers" onclick="clearMarkers()" /></form>
  • C#程式端,以Button3為例,這個按鈕的目的是取得B類的座標並繪製於地圖上
    protected void Button3_Click(object sender, EventArgs e)
    {
        Label1.Text = System.DateTime.Now.ToLongTimeString();
        Label1.Visible = true;

        string strJS = "";
        ArrayList alMarkers = GetMarkers("B");

        strJS = @"var sites = [" + String.Join(",", alMarkers.ToArray()) + "];setMarkers(sites);";
        ScriptManager.RegisterStartupScript(this.Page, this.Page.GetType(), "alert", strJS, true);
    }
  • C#程式端,GetMarkers的目的是從資料庫取出對應的
protected ArrayList GetMarkers(string strType)
    {
        ArrayList alMarkers = new ArrayList();
        try
        {
            string conn = ConfigurationManager.ConnectionStrings["mydevConnectionString"].ConnectionString;
            using (SqlConnection connection = new SqlConnection(conn))
            {
                connection.Open();

                SqlCommand command = connection.CreateCommand();
                command.Connection = connection;

                if (strType == "ALL")
                {
                    command.CommandText = @"SELECT * FROM [MyDev].[dbo].[MyMap] with (nolock)";
                }
                else
                {
                    command.CommandText = @"SELECT * FROM [MyDev].[dbo].[MyMap] with (nolock) where Category=@Category";
                    command.Parameters.Add("@Category", SqlDbType.NVarChar, 20).Value = strType;
                }

                using (SqlDataReader dr = command.ExecuteReader())
                {
                    while (dr.Read())
                    {
                        alMarkers.Add("['" + dr["Project"].ToString() + "', " + dr["Latitude"].ToString() + "," + dr["Longtitude"].ToString() + ", 1, '" + dr["Info"].ToString() + "
" + dr["Addr"].ToString() + "']");
                    }
                }
            }
        }
        catch (Exception ex)
        {
            lbErr.Text = ex.ToString();
            lbErr.Visible = true;
        }
        return alMarkers;
    }

  • Table的設計很簡單,主要就是 Latitude(緯度), Longtitude(經度),其他都是輔助描述的欄位
CREATE TABLE [dbo].[MyMap](
 [ID] [bigint] IDENTITY(1,1) NOT NULL,
 [Category] [nvarchar](20) NOT NULL,
 [Code] [nvarchar](20) NULL,
 [Info] [nvarchar](200) NULL,
 [Project] [nvarchar](50) NULL,
 [Addr] [nvarchar](100) NOT NULL,
 [Latitude] [float] NOT NULL,
 [Longtitude] [float] NOT NULL,
 [Coord] [geography] NULL
) ON [PRIMARY]


完成,結果如圖:
一進入網頁時,會直接定位目前座標,My Current Location 是我目前的座標,黃色是72公尺的精準度,其他的marker是點了B,帶入B類的座標,顯示在地圖上。

歡迎討論!

程式下載:
https://dl.dropbox.com/u/3330791/Sharecode/GoogleMapV3_ASPNET.rar

reference:
https://developers.google.com/maps/documentation/javascript/reference

2012年11月19日 星期一

[Toolkit] Get to Get coordinates in Images ? / 如何批次取得照片中的座標?

  • 目的:手邊有一些照片有座標資訊,現在需要取出座標內容進行後續的加值,除了一張一張照片打開看EXIF座標欄位以外,希望可以批次取得。
  • 使用工具:感謝這位日本網友 nissuk (https://github.com/nissuk),提供可用的powershell (https://gist.github.com/1360596),但是在中文環境下,會有問題,所以小弟有修改過,放在 http://dl.dropbox.com/u/3330791/get-exif.ps1 
  • 使用方式:
1 集合照片到同一個目錄,把 ps 直接放到到照片的目錄下

2 以Powershell執行 ps1,如果不只jpg,則請修改ps1的最後一行:
原本:dir "*.jpg" | get-exif | select name, latitude, longitude | export-csv
改為:dir "*.gif" | get-exif | select name, latitude, longitude | export-csv out.csv

2 執行結果:產生CSV檔案,這樣就完成了! 超級方便!


對這些有興趣的朋友,請看這個PowerShell Image module:
http://archive.msdn.microsoft.com/PSImage/

2012年10月19日 星期五

[Excel] 每個開發人員都需要知道的Excel公式


在這邊介紹幾個小弟經常用到的Excel公式,希望對大家使用Excel上有幫助。

1.字串處理
字串處理是用 Excel 給資訊人員最大的福利,尤其是需要批次處理時,不論是執行SQL、執行powershell 或整理報表,用Excel產生多筆的指令再去執行,真是省下很多作業的時間!

(1)合併Excel的欄位值:Excel的指令是=Concatenate(字串1, 字串2, 字串3...),譬如 =CONCATENATE("insert into tb  ([MyID],[MyDate1],[MyDate2],[MyCompany],[MyPosition],[Description])   values (",A2,",'",TEXT(C2,"yyyy/mm/dd"),"','",TEXT(D2,"yyyy/mm/dd"),"','",E2,"','",J2,"','",H2,"')")
的意思就是從A2~H2取欄位值出來,組合成 insert 指令。寫好第一行後,在Excel依次往下拉,就可以產生很多 insert 指令再貼到下Query的工具上,速度!

(2)取出固定文字的左邊或右邊:這種也常遇到,對於辦公室人員,Excel有些是要看的,但是不一定是結構性的資料,所以會有 "員工編號-XXX" 在同一個欄位出現,該如何處理呢 ? 譬如 1000-甲 要找出"-"左邊的ID,可以用 Find 這個公式找到"-"的位置,搭配Left,公式是 =LEFT(A1, FIND("-",A1)-1) ,長度要 "-1"是因為只要"100"。

如果要找右邊的,公式就寫成 =RIGHT(A1,LEN(A1)-FIND("-",A1))
公式的內容應該是不難懂,主要是組合多個公式起來,如果一開始不確定,也可以先個別計算,這樣就可以一步一步看結果。
(3)補0(或其他需要重複的字):主要是用REPT(重複字) 跟 LEN(值的文字長度)組合使用,譬如100,2000,30000都要補成 5碼的長度,公式是=CONCATENATE(REPT("0",5-LEN(B1)),B1),
分解步驟的話,公式的目的是
 - 重複0多次,要重複的次數:固定長度5 減掉100的長度(=3),所以是重複0共2次
 - 字串合併 00 與 100

2.資料取得對應值:在Excel 處理資料時,會遇到需要取得另一個表的值,也就是資料庫的join,可以使用: vlookup 這個公式,公式的格式是=Vlookup(來源的值,要查的範圍,要回傳的欄位位置,是否要完整比對)。
以下圖為例,公式是=VLOOKUP(A1,D:E,2,FALSE)  "我要找 A1 對應的值[第1個參數],去D與E構成的對應表去找[第2個參數],當A1對應到Excel Column D的值時,回傳第2行[第3個參數],比對時要完全正確才回傳[第4個參數]。
如果查不到,就會出現#N/A的訊息。

而#N/A是不是很刺眼呢,如果又要拿來串SQL指令時,#N/A會造成CONCATENATE失敗。
該如何避免呢?Excel有另一個公式叫做 ISError ,類似寫程式的try catch例外判斷,搭配IF,如: =IF(ISERROR(VLOOKUP(A1,D:E,2,FALSE)),"--",VLOOKUP(A1,D:E,2,FALSE))
意思是 "如果查到的結果是錯誤,就出現 --,如果不是錯誤,就出現查詢的結果",
看不到 #N/A,爽度也提高了

要注意的是
  • Vlookup第2個參數的對應表必須要排序過,要不然是查不到的。
  • 要拿來查詢的值與與對應表的值型態一樣才查的到,1(數字) 與 "1"(文字)是無法比對的上,用改格式的方式去處理會沒用,要用Text這個函數把數字改成文字,1才會變成"1"
  • 如果資料量很大,vlookup 搭配 iserror可能會花時間,不如copy結果出來成另一個Excel在交出去。
另外,
如果你的Excel很複雜,需要查多值,請google "excel array formula vlookup" 希望可以找到你要的答案。
Vlookup也可以自己查自己,因此可以做到類似 recursive 的效果,最適合在產生樹狀結構,如組織的結構,只要把key設為上層,就可以產生出 //公司//董事會//董事長//總經理//台灣分公司//資訊部 這樣的多層結構!

3.多IF的判斷:Excel 最單純的 IF是 =IF(條件,條件成立的值,條件不成立的值)。如果要做類似多層的IF判斷時,也就是要 Switch 時或 nested if ,Excel 也是用IF來達成,公式的範例

=IF(A1="A","1",
  IF(A1="B","2",
  IF(A1="C","3","N")
 ))
這樣Excel就會判斷
Switch(A1)
{
 case "A":"1"
 case "B":"2"
 case "C":"3"
 default: "N"
}
可以看圖也許比較清楚,公式內也可以按 ALT+Enter斷行作些簡單的程式排版,這樣就比較容易知道自己在寫些甚麼了!


Excel還有一些功能,像是清單整理出唯一值(如 SQL Distinct)、Column 轉 Row,都還滿常用的!
歡迎留言交流!

2012年5月12日 星期六

[Sharepoint 2010] 用Powershell建Sharepoint群組並指定權限 / Create Group Powershell and add permission


管理權限一直是各種系統的基礎重點,在Sharepoint跟AD的搭配上,都會以AGDLP去講該如何規劃設計。因此在管理 Sharepoint 群組的權限上,系統管理者(ㄞ  ㄊㄧ  ㄓㄨㄢ  ㄩㄢˊ)又要開熟悉Sharepoint Web/ Sharepoint Designer友善的畫面,以下要說明的是
1.在頂層網站建Group
2.把新建好的Group加到各下層的網站,並停止繼承
3.改下層網站各清單的權限


1.在頂層網站建Group,譬如有3個site
$web = Get-SPWeb "http://sps2010/"
$web.SiteGroups.Add("site1_A", $web.Site.Owner, $web.Site.Owner,"")
$web.SiteGroups.Add("site1_B", $web.Site.Owner, $web.Site.Owner,"")
$web.SiteGroups.Add("site1_C", $web.Site.Owner, $web.Site.Owner, "")
$web.SiteGroups.Add("site2_A", $web.Site.Owner, $web.Site.Owner,"")
$web.SiteGroups.Add("site2_B", $web.Site.Owner, $web.Site.Owner,"")
$web.SiteGroups.Add("site2_C", $web.Site.Owner, $web.Site.Owner, "")
$web.SiteGroups.Add("site3_A", $web.Site.Owner, $web.Site.Owner,"")
$web.SiteGroups.Add("site3_B", $web.Site.Owner, $web.Site.Owner,"")
$web.SiteGroups.Add("site3_C", $web.Site.Owner, $web.Site.Owner, "")
$web.Update()
$web.Dispose()

這一段沒什麼學問,如果需要一次建很多,其實可以用EXCEL去組合上面的字。建完Group後,如果需要先放群組的權限,建議用Sharepoint Designer,用Web去管理會在那邊等等等...

2.把新建好的Group加到各下層的網站,並停止繼承
###########################
#
# 函示:add group permission
#
###########################
function AddGroupToSite ($web, $groupName, $permLevel)
{
    $account = $web.SiteGroups[$groupName]
    $assignment = New-Object Microsoft.SharePoint.SPRoleAssignment($account)
    $role = $web.RoleDefinitions[$permLevel]
    $assignment.RoleDefinitionBindings.Add($role);
    $web.RoleAssignments.Add($assignment)
}
#########

#Subsites待修改:看有哪些子網站要執行
$SubSites = @("site1","site2","site3"
)
for($i=0 ; $i -lt $SubSites.count ; $i++)
{
 $url = "http://sps2010/" + $SubSites[$i]
 $web = Get-SPWeb $url
 $web.BreakRoleInheritance($false)
 #Subsites待修改,看Group的名字
 $grp1 = $SubSites[$i]+"_A"
 $grp2 = $SubSites[$i]+"_B"
 $grp3 = $SubSites[$i]+"_C"
 AddGroupToSite -web $web -groupName $grp1 -permLevel "Read"
 AddGroupToSite -web $web -groupName $grp2 -permLevel "Read"
 AddGroupToSite -web $web -groupName $grp3 -permLevel "Read"
 $web.Dispose() 
 Write-Output $SubSites[$i] + " Completed!"
}

3.改下層網站各清單的權限
$web = Get-SPWeb "http://sps2010/"
#List Permission
#
#兩個清單要改:
#ListA: url是 /Site1/List/ListA
#ListB: url是 /Site1/SitePicLib
#如果有更多要改,就一直加在函示裡面
#
###########################
#
# 函示:Change List Permission
#
###########################
function ChangeListPermission ($strSiteName)
{
 $SubSites = $strSiteName
 $grp1 = $SubSites+"_A"
 $grp2 = $SubSites+"_B"
 $grp3 = $SubSites+"_C"
 $url = "http://sps2010/" + $SubSites
 $web = Get-SPWeb $url
 $admaccount = $web.EnsureUser("SHAREPOINT\system")
 #############################ListA#############################
 #$ListR = $web.Lists["ListA"]
 $strListURL = "/" + $SubSites + "/Lists/ListA"
 $ListR = $web.GetList($strListURL)
 $ListR.BreakRoleInheritance($false)
 $account = $web.SiteGroups[$grp2]
 $assignment = New-Object Microsoft.SharePoint.SPRoleAssignment($account)
 $assignment.RoleDefinitionBindings.Add(($web.RoleDefinitions | Where-Object { $_.Type -eq "Contributor" }))
 $ListR.RoleAssignments.Add($assignment)
 $account = $web.SiteGroups[$grp4]
 $assignment = New-Object Microsoft.SharePoint.SPRoleAssignment($account)
 $assignment.RoleDefinitionBindings.Add(($web.RoleDefinitions | Where-Object { $_.Type -eq "Contributor" }))
 $ListR.RoleAssignments.Add($assignment)
 $ListR.RoleAssignments.Remove($admaccount)
 #############################ListB#############################
 $strListURL = "/" + $SubSites + "/SitePicLib"
 $ListR = $web.GetList($strListURL)
 $ListR.BreakRoleInheritance($false)
 $account = $web.SiteGroups[$grp2]
 $assignment = New-Object Microsoft.SharePoint.SPRoleAssignment($account)
 $assignment.RoleDefinitionBindings.Add(($web.RoleDefinitions | Where-Object { $_.Type -eq "Contributor" }))
 $ListR.RoleAssignments.Add($assignment)
 $account = $web.SiteGroups[$grp4]
 $assignment = New-Object Microsoft.SharePoint.SPRoleAssignment($account)
 $assignment.RoleDefinitionBindings.Add(($web.RoleDefinitions | Where-Object { $_.Type -eq "Contributor" }))
 $ListR.RoleAssignments.Add($assignment)
 $account = $web.SiteGroups[$grp1]
 $assignment = New-Object Microsoft.SharePoint.SPRoleAssignment($account)
 $assignment.RoleDefinitionBindings.Add($web.RoleDefinitions["Read"])
 $ListR.RoleAssignments.Add($assignment)
 $account = $web.SiteGroups[$grp3]
 $assignment = New-Object Microsoft.SharePoint.SPRoleAssignment($account)
 $assignment.RoleDefinitionBindings.Add($web.RoleDefinitions["Read"])
 $ListR.RoleAssignments.Add($assignment)
 $ListR.RoleAssignments.Remove($admaccount)
 $web.Dispose()
}
###########################

#待修改
#實際呼叫函示
ChangeListPermission -strSiteName "site1"
ChangeListPermission -strSiteName "site2"
ChangeListPermission -strSiteName "site3"

權限這樣就差不多設定完成了,搭配Excel更快!
如果要對Sharepoint Group加AD Group,請看下一篇!


參考資料:
1.PowerShell to create SharePoint groups http://blog.pointbeyond.com/2011/06/03/powershell-to-create-sharepoint-groups/


#DontLikeSP

2012年3月1日 星期四

[Sharepoint 2010] 用Powershell 建網站 / Create web with Powershell

有鑑於 Sharepoint建網站的UI太友善了,讓要建多個網站的管理員會非常熟悉建站的動作跟設定,因此小弟到蒐集網路上各種建站的Powershell,東拼西湊成一個建多個站的Powershell:

1.如果有自訂的web template,則需要找出ID跟Name,可用:

 #待修改
 $url = "http://sps2010/"
 $site= new-Object Microsoft.SharePoint.SPSite($url )
 $loc= [System.Int32]::Parse(1033)
 $templates= $site.GetWebTemplates($loc)
   foreach ($child in $templates)
      {
               write-host $child.Name "  " $child.Title
      }
 $site.Dispose()





查詢出的結果會類似
{2F1B367A-5FF5-444E-B3BC-DBB73E1FEDXX}#SiteTemplateName    SiteTemplate_Title
只需要用到前面的,Title只是用來識別的

2.create it!
 以下的Powershell有包含以下步驟
 (1).建立一個新的網站
 (2).設定 "網站設定"的"其他語言",增加中文(1028)
 (3).設定 "網站設定"的"覆寫翻譯"
 (4).設定 "管理網站功能"的 "SharePoint Server Publishing"(為了master page)
 (5).設定 Master Page


#待修改

$SiteCollectionURL = "http://sps2010"

#待修改


$SiteCollectionTemplate = "
{2F1B367A-5FF5-444E-B3BC-DBB73E1FEDXX}#SiteTemplateName "

#預設語系英文

$SiteCollectionLanguage = 1033

#新站的URL

$SubSites = @("UrlA",

"UrlA",

"UrlA",

)

#新站的標題(Title)

$SubSiteNames =@("TitleA",

"TitleB",

"TitleC",

)

for($i=0 ; $i -lt $SubSites.count ; $i++)

{

 #(1).建立一個新的網站,根據指定的範本與語系,描述則預設為空白

 $SiteUrl = ""

 $SiteUrl = $SiteCollectionURL + "/"

 $SiteUrl = $SiteUrl += $SubSites[$i]

 $web=New-SPWeb $SiteUrl -Name $SubSiteNames[$i]  -UseParentTopNav -Language $SiteCollectionLanguage -Description " "
 #在我的環境,要再套用一次才會work, 不知道為什麼!
 $web.ApplyWebTemplate($SiteCollectionTemplate) 

 #(2).設定 "網站設定"的"其他語言",增加中文(1028)

 $web.IsMultilingual = $true

 $spReg = New-Object Microsoft.SharePoint.SPRegionalSettings $web

 #顯示安裝的language pack

 $spReg.InstalledLanguages

 $web.AddSupportedUICulture(1028)

 #(3).設定 "網站設定"的"覆寫翻譯"

 $web.OverwriteTranslationsOnChange = $true;

 #(4).設定 "管理網站功能"的 "SharePoint Server Publishing"(為了master page)

 Enable-SPFeature -identity "PublishingWeb" -URL $SiteUrl

 $web.Dispose()

 #Write-Output "完成: " += $SubSites[$i]


}

#

#(5).設定 Master Page

#因為需要web.update 所以分開執行

#

for($i=0 ; $i -lt $SubSites.count ; $i++)

{

 $SiteUrl = ""

 $SiteUrl = $SiteCollectionURL + "/"

 $SiteUrl = $SiteUrl += $SubSites[$i]

 $web = Get-SPWeb $SiteUrl

 $web.AllProperties["__InheritsCustomMasterUrl"] = "False";

 #待修改

 $web.CustomMasterUrl = "/_catalogs/masterpage/myv4.master"

 $web.AllProperties["__InheritsMasterUrl"] = "False";

  #待修改

 $web.MasterUrl = "/_catalogs/masterpage/myv4.master"

 $web.Update()

 $web.Dispose()

}


希望sharepoint後代多考慮管理員批次處理的功能,時間不是用來做重複的管理工作!

參考:



#DontLikeSP

2012年2月18日 星期六

[How to] 如何用C#使用Windows Fax / Fax with C# in Windows Fax Service Environment

[How to] Fax with C# in Windows Fax Service Environment
透過 Windows Fax Service,以程式方式發送傳真
  • 使用語言: C#
  • 加入參考 ( FXSRESM.dll ) 路徑在: C:\windows\system32\FXSRESM.dll
  • Code 如下:
    FaxServer faxServer = new FaxServer();
    //IP是Fax Server IP
    faxServer.Connect("192.168.1.1");
    FaxDocument aDoc = new FaxDocument();
    //要FAx的檔案
    aDoc.Body = "C:\\Temp\\1.docx";
    aDoc.ReceiptAddress = "stace@xxx";
    //收傳真的電話
    aDoc.Recipients.Add("88880000", "Recp");
    aDoc.ConnectedSubmit(faxServer);

  • 就這樣就可以發Fax出去, Stupid and simple! Good!
  • 其他傳真的封面/封底/Server傳送的狀態還需要再詳細看SDK與 COM Object的其他method! 基本上應該還要確認一下對方是否有傳成功,就類似人為動作一樣..

參考:

  • Using the Fax Service SDK http://msdn.microsoft.com/en-us/library/windows/desktop/ms693392(v=vs.85).aspx
  • Fax Service Extended COM Objects http://msdn.microsoft.com/en-us/library/windows/desktop/ms693456(v=vs.85).aspx