studio Odyssey



しゃちょのスタジオ日誌

 日記的なもの。

2006.12.22

Windows Mobile はよくわからん

手動トラックバック先URI
http://www.studio-odyssey.net/content/note/archive01.htm#l2206

 Connection Managerを通して接続しても、通信が間に発生すると、どうにも切断されない。

 これは仕様なんだろうか…つーか、非常にむかつく。Disconnectとアプリケーション開発者が言っているのだから、OSは素直に切断しろよと言いたい。つーか、つなげたの俺だし、ハンドル持ってるのも俺だし!

 とまれ、切れないのでむかつく。
 ので、どうしたのかというと、結局RASのお世話になった。

 RasEnumConnectionsでRASの接続先リストが取れるので、それで接続しているものを調べる。つーか、Connection Manger経由でも、接続するとRASに登録されるのね。何の意味があるんだ、Connection Manager。逆は認識しないくせに。

 で、接続のリストが取れれば、切断はRasHangUpで、RASCONのhrasconnを送ってあげれば切断できる。この切断はConnection Managerが認識するので、ちゃんとアイコンが変わる。なんでやねん。

 Windows Mobile5.0は謎OSだな。

 つーか、このあたりは.net frameworkでサポートしてくれんのかね?
 欧米ではこういう使い方しないのかね…日本だけなのかね…

 つなげたら切断、なんていうのは、日本だけの文化の気もしないでもないけど。


2006.12.12

Windows Mobile Connection Manager の完全なコード(2)

手動トラックバック先URI
http://www.studio-odyssey.net/content/note/archive01.htm#l1206

 前回のエントリ、Windows Mobile Connection Manager の完全なコードに間違いがあったので、訂正した。
 本来なら、delでマークアップするなりして訂正すべきだが、コードそのものに間違いがあったので、完全に修正した。ご容赦いただきたい。

 つーか、俺のコードなんて、間違ってるもんだよ!

 さて、それはともかく、だ。
 仕事の上でConnectionManagerをうりうりしたいというのは、はっきり言えば「このアプリでは、指定された場所にダイヤルアップしたい」なわけだ。つーかそうだろ?
 はてさて、で、ConnectionManager APIでそれが出来るかというと、はっきり言おう。

 出来ない。

 strongでマークしちゃうくらいですよ。言い切りますよ。
 出来ない。

 出来るなら教えてもらいたいくらいだ。

 RAS?
 RASはダメです。W-ZERO3の場合(たぶん、WindowsMobileの仕様)、RASでつなげると、パケットモードを認識しないで、画面上部の電波マークが、データ通信モードになりません。これがならないという事は、タップで切断出来ないという事です。これはたぶん、MS的な仕様に準拠しないので、やるべきではないのでしょう。

 余談ですが、C#でのRAS接続のコードは以下です。試しに書いただけなので、こんなものが業務に使えるわけはないですが、まぁ、探している人がいれば、どうぞ。

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;

namespace BCL.WindowsMobile.Connection
{
   /// <summary>
   /// 結局、RASの助けを借りないと、何も出来ないの。
   /// </summary>
   public class RasManager
   {
      [DllImport("Coredll.dll", EntryPoint = "RasEnumEntries", SetLastError = true)]
      internal static extern uint RasEnumEntries(string Reserved, string lpszPhoneBookPath, IntPtr lprasentryname, ref uint lpcb, ref uint lpcEntries);

      /*DWORD RasEnumEntries(
LPWSTR Reserved,
LPWSTR lpszPhoneBookPath,
LPRASENTRYNAME lprasentryname,
LPDWORD lpcb,
LPDWORD lpcEntries
);*/
      [DllImport("Coredll.dll", EntryPoint = "RasEnumConnections", SetLastError = true)]
      private unsafe static extern uint RasEnumConnections(IntPtr lprasentryname, ref uint lpcb, ref uint lpcConnections);


    /* DWORD RasEnumConnections(
LPRASCONN lprasconn,
LPDWORD lpcb,
LPDWORD lpcConnections
);*/
      [DllImport("Coredll.dll", EntryPoint = "RasGetConnectStatus", SetLastError = true)]
      private unsafe static extern uint RasGetConnectStatus(IntPtr rasconn, ref RASCONNSTATUS lprasconnstatus);

      /*
       * DWORD RasGetConnectStatus(
HRASCONN rasconn,
LPRASCONNSTATUS lprasconnstatus
);
       */

      const int RAS_MaxEntryName = 20;
      public const int RAS_MaxDeviceName =   128;
      public const int RAS_MaxDeviceType = 16;

      [StructLayout(LayoutKind.Sequential)]
      private struct RASENTRYNAME
      {
         public uint dwSize;
         [MarshalAs(UnmanagedType.ByValTStr, SizeConst = RAS_MaxEntryName + 1)]
         public string szEntryName;
      }
      /*typedef struct _RASENTRYNAME {
DWORD dwSize;
TCHAR szEntryName[ RAS_MaxEntryName + 1 ];
} RASENTRYNAME;*/

      [StructLayout(LayoutKind.Sequential)]
      public struct RASCONN
      {
         public uint dwSize;
         public IntPtr hrasconn;
         
         [MarshalAs(UnmanagedType.ByValTStr, SizeConst = RAS_MaxEntryName + 1)]
         public string szEntryName;

      }


      /*
       * typedef struct _RASCONN {
DWORD dwSize;
HRASCONN hrasconn;
TCHAR szEntryName[ RAS_MaxEntryName + 1 ];
} RASCONN;
       * */

      [StructLayout(LayoutKind.Sequential)]
      public struct RASCONNSTATUS
      {
         public uint dwSize;
         public RASCONNSTATE rasconnstate;
         public uint dwError;
         [MarshalAs(UnmanagedType.ByValTStr, SizeConst = RAS_MaxDeviceType + 1)]
         public string szDeviceType;
         [MarshalAs(UnmanagedType.ByValTStr, SizeConst = RAS_MaxDeviceName + 1)]
         public string szDeviceName;

      }
                /* typedef struct _RASCONNSTATUS {
DWORD dwSize;
RASCONNSTATE rasconnstate;
DWORD dwError;
TCHAR szDeviceType[ RAS_MaxDeviceType + 1 ];
TCHAR szDeviceName[ RAS_MaxDeviceName + 1 ];
} RASCONNSTATUS;*/

      const int RASCS_PAUSED = 0x1000;
      const int RASCS_DONE = 0x2000;

      public enum RASCONNSTATE
      {
         RASCS_OpenPort = 0,
         RASCS_PortOpened,
         RASCS_ConnectDevice,
         RASCS_DeviceConnected,
         RASCS_AllDevicesConnected,
         RASCS_Authenticate,
         RASCS_AuthNotify,
         RASCS_AuthRetry,
         RASCS_AuthCallback,
         RASCS_AuthChangePassword,
         RASCS_AuthProject,
         RASCS_AuthLinkSpeed,
         RASCS_AuthAck,
         RASCS_ReAuthenticate,
         RASCS_Authenticated,
         RASCS_PrepareForCallback,
         RASCS_WaitForModemReset,
         RASCS_WaitForCallback,
         RASCS_Projected,
         RASCS_Interactive = RASCS_PAUSED,
         RASCS_RetryAuthentication,
         RASCS_CallbackSetByCaller,
         RASCS_PasswordExpired,
         RASCS_Connected = RASCS_DONE,
         RASCS_Disconnected
      }
      /*typedef enum _RASCONNSTATE {
RASCS_OpenPort = 0,
RASCS_PortOpened,
RASCS_ConnectDevice,
RASCS_DeviceConnected,
RASCS_AllDevicesConnected,
RASCS_Authenticate,
RASCS_AuthNotify,
RASCS_AuthRetry,
RASCS_AuthCallback,
RASCS_AuthChangePassword,
RASCS_AuthProject,
RASCS_AuthLinkSpeed,
RASCS_AuthAck,
RASCS_ReAuthenticate,
RASCS_Authenticated,
RASCS_PrepareForCallback,
RASCS_WaitForModemReset,
RASCS_WaitForCallback,
RASCS_Projected,
RASCS_Interactive = RASCS_PAUSED,
RASCS_RetryAuthentication,
RASCS_CallbackSetByCaller,
RASCS_PasswordExpired,
RASCS_Connected = RASCS_DONE,
RASCS_Disconnected
} RASCONNSTATE;*/


      const uint RASBASE = 600;
      const uint SUCCESS = 0;
      /*
#define RAS_MaxEntryName    20
#define RAS_MaxDeviceName    128
#define RAS_MaxDeviceType    16
#define RAS_MaxParamKey    32
#define RAS_MaxParamValue    128
#define RAS_MaxPhoneNumber   128
#define RAS_MaxCallbackNumber 48
#define RAS_MaxIpAddress    15
#define RAS_MaxIpxAddress    21*/

      //uint PENDING = (RASBASE + 0);
      //uint ERROR_INVALID_PORT_HANDLE = (RASBASE + 1);
      //uint ERROR_PORT_ALREADY_OPEN = (RASBASE + 2);
      const uint ERROR_BUFFER_TOO_SMALL = (RASBASE + 3);
      //uint ERROR_WRONG_INFO_SPECIFIED = (RASBASE + 4);
      //uint ERROR_CANNOT_SET_PORT_INFO = (RASBASE + 5);
      //uint ERROR_PORT_NOT_CONNECTED = (RASBASE + 6);
      //uint ERROR_EVENT_INVALID = (RASBASE + 7);
      //uint ERROR_DEVICE_DOES_NOT_EXIST = (RASBASE + 8);
      //uint ERROR_DEVICETYPE_DOES_NOT_EXIST = (RASBASE + 9);
      //uint ERROR_BUFFER_INVALID = (RASBASE + 10);
      //uint ERROR_ROUTE_NOT_AVAILABLE = (RASBASE + 11);
      //uint ERROR_ROUTE_NOT_ALLOCATED = (RASBASE + 12);
      //uint ERROR_INVALID_COMPRESSION_SPECIFIED = (RASBASE + 13);
      //uint ERROR_OUT_OF_BUFFERS = (RASBASE + 14);
      //uint ERROR_PORT_NOT_FOUND = (RASBASE + 15);
      //uint ERROR_ASYNC_REQUEST_PENDING = (RASBASE + 16);
      //uint ERROR_ALREADY_DISCONNECTING = (RASBASE + 17);
      //uint ERROR_PORT_NOT_OPEN = (RASBASE + 18);
      //uint ERROR_PORT_DISCONNECTED = (RASBASE + 19);
      //uint ERROR_NO_ENDPOINTS = (RASBASE + 20);
      //uint ERROR_CANNOT_OPEN_PHONEBOOK = (RASBASE + 21);
      //uint ERROR_CANNOT_LOAD_PHONEBOOK = (RASBASE + 22);
      //uint ERROR_CANNOT_FIND_PHONEBOOK_ENTRY = (RASBASE + 23);
      //uint ERROR_CANNOT_WRITE_PHONEBOOK = (RASBASE + 24);
      //uint ERROR_CORRUPT_PHONEBOOK = (RASBASE + 25);
      //uint ERROR_CANNOT_LOAD_STRING = (RASBASE + 26);
      //uint ERROR_KEY_NOT_FOUND = (RASBASE + 27);
      //uint ERROR_DISCONNECTION = (RASBASE + 28);
      //uint ERROR_REMOTE_DISCONNECTION = (RASBASE + 29);
      //uint ERROR_HARDWARE_FAILURE = (RASBASE + 30);
      //uint ERROR_USER_DISCONNECTION = (RASBASE + 31);
      //uint ERROR_INVALID_SIZE = (RASBASE + 32);
      //uint ERROR_PORT_NOT_AVAILABLE = (RASBASE + 33);
      //uint ERROR_CANNOT_PROJECT_CLIENT = (RASBASE + 34);
      //uint ERROR_UNKNOWN = (RASBASE + 35);
      //uint ERROR_WRONG_DEVICE_ATTACHED = (RASBASE + 36);
      //uint ERROR_BAD_STRING = (RASBASE + 37);
      //uint ERROR_REQUEST_TIMEOUT = (RASBASE + 38);
      //uint ERROR_CANNOT_GET_LANA = (RASBASE + 39);
      //uint ERROR_NETBIOS_ERROR = (RASBASE + 40);
      //uint ERROR_SERVER_OUT_OF_RESOURCES = (RASBASE + 41);
      //uint ERROR_NAME_EXISTS_ON_NET = (RASBASE + 42);
      //uint ERROR_SERVER_GENERAL_NET_FAILURE = (RASBASE + 43);
      //uint WARNING_MSG_ALIAS_NOT_ADDED = (RASBASE + 44);
      //uint ERROR_AUTH_INTERNAL = (RASBASE + 45);
      //uint ERROR_RESTRICTED_LOGON_HOURS = (RASBASE + 46);
      //uint ERROR_ACCT_DISABLED = (RASBASE + 47);
      //uint ERROR_PASSWD_EXPIRED = (RASBASE + 48);
      //uint ERROR_NO_DIALIN_PERMISSION = (RASBASE + 49);
      //uint ERROR_SERVER_NOT_RESPONDING = (RASBASE + 50);
      //uint ERROR_FROM_DEVICE = (RASBASE + 51);
      //uint ERROR_UNRECOGNIZED_RESPONSE = (RASBASE + 52);
      //uint ERROR_MACRO_NOT_FOUND = (RASBASE + 53);
      //uint ERROR_MACRO_NOT_DEFINED = (RASBASE + 54);
      //uint ERROR_MESSAGE_MACRO_NOT_FOUND = (RASBASE + 55);
      //uint ERROR_DEFAULTOFF_MACRO_NOT_FOUND = (RASBASE + 56);
      //uint ERROR_FILE_COULD_NOT_BE_OPENED = (RASBASE + 57);
      //uint ERROR_DEVICENAME_TOO_LONG = (RASBASE + 58);
      //uint ERROR_DEVICENAME_NOT_FOUND = (RASBASE + 59);
      //uint ERROR_NO_RESPONSES = (RASBASE + 60);
      //uint ERROR_NO_COMMAND_FOUND = (RASBASE + 61);
      //uint ERROR_WRONG_KEY_SPECIFIED = (RASBASE + 62);
      //uint ERROR_UNKNOWN_DEVICE_TYPE = (RASBASE + 63);
      //uint ERROR_ALLOCATING_MEMORY = (RASBASE + 64);
      //uint ERROR_PORT_NOT_CONFIGURED = (RASBASE + 65);
      //uint ERROR_DEVICE_NOT_READY = (RASBASE + 66);
      //uint ERROR_READING_INI_FILE = (RASBASE + 67);
      //uint ERROR_NO_CONNECTION = (RASBASE + 68);
      //uint ERROR_BAD_USAGE_IN_INI_FILE = (RASBASE + 69);
      //uint ERROR_READING_SECTIONNAME = (RASBASE + 70);
      //uint ERROR_READING_DEVICETYPE = (RASBASE + 71);
      //uint ERROR_READING_DEVICENAME = (RASBASE + 72);
      //uint ERROR_READING_USAGE = (RASBASE + 73);
      //uint ERROR_READING_MAXCONNECTBPS = (RASBASE + 74);
      //uint ERROR_READING_MAXCARRIERBPS = (RASBASE + 75);
      //uint ERROR_LINE_BUSY = (RASBASE + 76);
      //uint ERROR_VOICE_ANSWER = (RASBASE + 77);
      //uint ERROR_NO_ANSWER = (RASBASE + 78);
      //uint ERROR_NO_CARRIER = (RASBASE + 79);
      //uint ERROR_NO_DIALTONE = (RASBASE + 80);
      //uint ERROR_IN_COMMAND = (RASBASE + 81);
      //uint ERROR_WRITING_SECTIONNAME = (RASBASE + 82);
      //uint ERROR_WRITING_DEVICETYPE = (RASBASE + 83);
      //uint ERROR_WRITING_DEVICENAME = (RASBASE + 84);
      //uint ERROR_WRITING_MAXCONNECTBPS = (RASBASE + 85);
      //uint ERROR_WRITING_MAXCARRIERBPS = (RASBASE + 86);
      //uint ERROR_WRITING_USAGE = (RASBASE + 87);
      //uint ERROR_WRITING_DEFAULTOFF = (RASBASE + 88);
      //uint ERROR_READING_DEFAULTOFF = (RASBASE + 89);
      //uint ERROR_EMPTY_INI_FILE = (RASBASE + 90);
      //uint ERROR_AUTHENTICATION_FAILURE = (RASBASE + 91);
      //uint ERROR_PORT_OR_DEVICE = (RASBASE + 92);
      //uint ERROR_NOT_BINARY_MACRO = (RASBASE + 93);
      //uint ERROR_DCB_NOT_FOUND = (RASBASE + 94);
      //uint ERROR_STATE_MACHINES_NOT_STARTED = (RASBASE + 95);
      //uint ERROR_STATE_MACHINES_ALREADY_STARTED = (RASBASE + 96);
      //uint ERROR_PARTIAL_RESPONSE_LOOPING = (RASBASE + 97);
      //uint ERROR_UNKNOWN_RESPONSE_KEY = (RASBASE + 98);
      //uint ERROR_RECV_BUF_FULL = (RASBASE + 99);
      //uint ERROR_CMD_TOO_LONG = (RASBASE + 100);
      //uint ERROR_UNSUPPORTED_BPS = (RASBASE + 101);
      //uint ERROR_UNEXPECTED_RESPONSE = (RASBASE + 102);
      //uint ERROR_INTERACTIVE_MODE = (RASBASE + 103);
      //uint ERROR_BAD_CALLBACK_NUMBER = (RASBASE + 104);
      //uint ERROR_INVALID_AUTH_STATE = (RASBASE + 105);
      //uint ERROR_WRITING_INITBPS = (RASBASE + 106);
      //uint ERROR_X25_DIAGNOSTIC = (RASBASE + 107);
      //uint ERROR_ACCT_EXPIRED = (RASBASE + 108);
      //uint ERROR_CHANGING_PASSWORD = (RASBASE + 109);
      //uint ERROR_OVERRUN = (RASBASE + 110);
      //uint ERROR_RASMAN_CANNOT_INITIALIZE = (RASBASE + 111);
      //uint ERROR_BIPLEX_PORT_NOT_AVAILABLE = (RASBASE + 112);
      //uint ERROR_NO_ACTIVE_ISDN_LINES = (RASBASE + 113);
      //uint ERROR_NO_ISDN_CHANNELS_AVAILABLE = (RASBASE + 114);
      //uint ERROR_TOO_MANY_LINE_ERRORS = (RASBASE + 115);
      //uint ERROR_IP_CONFIGURATION = (RASBASE + 116);
      //uint ERROR_NO_IP_ADDRESSES = (RASBASE + 117);
      //uint ERROR_PPP_TIMEOUT = (RASBASE + 118);
      //uint ERROR_PPP_REMOTE_TERMINATED = (RASBASE + 119);
      //uint ERROR_PPP_NO_PROTOCOLS_CONFIGURED = (RASBASE + 120);
      //uint ERROR_PPP_NO_RESPONSE = (RASBASE + 121);
      //uint ERROR_PPP_INVALID_PACKET = (RASBASE + 122);
      //uint ERROR_PHONE_NUMBER_TOO_LONG = (RASBASE + 123);
      //uint ERROR_IPXCP_NO_DIALOUT_CONFIGURED = (RASBASE + 124);
      //uint ERROR_IPXCP_NO_DIALIN_CONFIGURED = (RASBASE + 125);
      //uint ERROR_IPXCP_DIALOUT_ALREADY_ACTIVE = (RASBASE + 126);
      //uint ERROR_ACCESSING_TCPCFGDLL = (RASBASE + 127);
      //uint ERROR_NO_IP_RAS_ADAPTER = (RASBASE + 128);
      //uint ERROR_SLIP_REQUIRES_IP = (RASBASE + 129);
      //uint ERROR_PROJECTION_NOT_COMPLETE = (RASBASE + 130);
      //uint ERROR_PROTOCOL_NOT_CONFIGURED = (RASBASE + 131);
      //uint ERROR_PPP_NOT_CONVERGING = (RASBASE + 132);
      //uint ERROR_PPP_CP_REJECTED = (RASBASE + 133);
      //uint ERROR_PPP_LCP_TERMINATED = (RASBASE + 134);
      //uint ERROR_PPP_REQUIRED_ADDRESS_REJECTED = (RASBASE + 135);
      //uint ERROR_PPP_NCP_TERMINATED = (RASBASE + 136);
      //uint ERROR_PPP_LOOPBACK_DETECTED = (RASBASE + 137);
      //uint ERROR_PPP_NO_ADDRESS_ASSIGNED = (RASBASE + 138);
      //uint ERROR_CANNOT_USE_LOGON_CREDENTIALS = (RASBASE + 139);
      //uint ERROR_TAPI_CONFIGURATION = (RASBASE + 140);
      //uint ERROR_NO_LOCAL_ENCRYPTION = (RASBASE + 141);
      //uint ERROR_NO_REMOTE_ENCRYPTION = (RASBASE + 142);
      //uint ERROR_REMOTE_REQUIRES_ENCRYPTION = (RASBASE + 143);
      //uint ERROR_IPXCP_NET_NUMBER_CONFLICT = (RASBASE + 144);
      //uint ERROR_INVALID_SMM = (RASBASE + 145);
      //uint ERROR_SMM_UNINITIALIZED = (RASBASE + 146);
      //uint ERROR_NO_MAC_FOR_PORT = (RASBASE + 147);
      //uint ERROR_SMM_TIMEOUT = (RASBASE + 148);
      //uint ERROR_BAD_PHONE_NUMBER = (RASBASE + 149);
      //uint ERROR_WRONG_MODULE = (RASBASE + 150);
      //uint ERROR_PPP_MAC = (RASBASE + 151);
      //uint ERROR_PPP_LCP = (RASBASE + 152);
      //uint ERROR_PPP_AUTH = (RASBASE + 153);
      //uint ERROR_PPP_NCP = (RASBASE + 154);
      //uint ERROR_POWER_OFF = (RASBASE + 155);
      //uint ERROR_POWER_OFF_CD = (RASBASE + 156);


      //uint ERROR_DIAL_ALREADY_IN_PROGRESS = (RASBASE + 157);
      //uint ERROR_RASAUTO_CANNOT_INITIALIZE = (RASBASE + 158);
      //uint ERROR_UNABLE_TO_AUTHENTICATE_SERVER = (RASBASE + 178);


      //RAS接続のてすと
      public static void RasDial()
      {
         System.Diagnostics.Process.Start("rnaapp.exe", "-e\"CLUB AIR-EDGE\" -m -p");
      }


      public static RASCONNSTATUS RasGetConnectStatus(IntPtr hrasconn)
      {
         RASCONNSTATUS rcs = new RASCONNSTATUS();
         rcs.dwSize = (uint)Marshal.SizeOf(rcs);

         if (RasGetConnectStatus(hrasconn, ref rcs) != SUCCESS)
         {
            
         }
         else
         {
            

         }
         return rcs;
      }

      /// <summary>
      /// 接続されているもの。
      /// </summary>
      /// <returns></returns>
      public static RASCONN[] RasConnections()
      {
         //基本的にやること一緒かよ
         Type strType = typeof(RASCONN);
         int strSize = Marshal.SizeOf(strType);

         RASCONN[] lprasentryname = new RASCONN[1];
         lprasentryname[0].dwSize = (uint)strSize;

         byte[] arrByte = ConvertToByte(lprasentryname);

         //APIに上げるポインタとかの準備
         IntPtr p = Marshal.AllocHGlobal(arrByte.Length);

         uint lpcEntries = 1;
         uint lpcb = (uint)arrByte.Length;

         try
         {
            uint apiResult = 0;

            if ((apiResult = RasEnumConnections(p, ref lpcb, ref lpcEntries)) == ERROR_BUFFER_TOO_SMALL)
            {
               //足りないので広げる
               int arrayLength = (int)(lpcb / strSize);

               lprasentryname = new RASCONN[arrayLength];

               for (int i = 0; i < arrayLength; i++)
               {
                  lprasentryname[i].dwSize = (uint)strSize;
               }

               Marshal.FreeHGlobal(p);
               p = IntPtr.Zero;

               arrByte = ConvertToByte(lprasentryname);
               p = Marshal.AllocHGlobal(arrByte.Length);

               apiResult = RasEnumConnections(p, ref lpcb, ref lpcEntries);
            }

            if (apiResult != SUCCESS)
            {
               return null;
            }
            else
            {
               arrByte = new Byte[lpcb];

               Marshal.Copy(p, arrByte, 0, (int)lpcb);
               Marshal.FreeHGlobal(p);
               p = IntPtr.Zero;

               RASCONN[] result = new RASCONN[lpcEntries];

               for (int i = 0; i < (int)lpcEntries; i++)
               {
                  p = Marshal.AllocHGlobal(strSize);
                  Marshal.Copy(arrByte, (strSize * i), p, (int)strSize);

                  lprasentryname[i] = (RASCONN)Marshal.PtrToStructure(p, strType);

                  result[i] = lprasentryname[i];

                  Marshal.FreeHGlobal(p);
                  p = IntPtr.Zero;
               }

               return result;
            }
         }
         finally
         {
            if (p != IntPtr.Zero)
               Marshal.FreeHGlobal(p);
            p = IntPtr.Zero;
         }
      }





      /// <summary>
      /// RASの接続先リストを取得する。
      /// </summary>
      /// <returns>接続先リスト。</returns>
      public static string[] RasEntries()
      {
         //よく使うので
         Type strType = typeof(RASENTRYNAME);
         int strSize = Marshal.SizeOf(strType);

         RASENTRYNAME[] lprasentryname = new RASENTRYNAME[1];
         lprasentryname[0].dwSize = (uint)strSize;

         byte[] arrByte = ConvertToByte(lprasentryname);

         //APIに上げるポインタとかの準備
         IntPtr p = Marshal.AllocHGlobal(arrByte.Length);

         uint lpcEntries = 1;
         uint lpcb = (uint)arrByte.Length;

         try
         {
            uint apiResult = 0;

            if ((apiResult = RasEnumEntries(null, null, p, ref lpcb, ref lpcEntries)) == ERROR_BUFFER_TOO_SMALL)
            {
               //足りないので広げる
               int arrayLength = (int)(lpcb / strSize);

               lprasentryname = new RASENTRYNAME[arrayLength];

               for (int i = 0; i < arrayLength; i++)
               {
                  lprasentryname[i].dwSize = (uint)strSize;
               }

               Marshal.FreeHGlobal(p);
               p = IntPtr.Zero;

               arrByte = ConvertToByte(lprasentryname);
               p = Marshal.AllocHGlobal(arrByte.Length);

               apiResult = RasEnumEntries(null, null, p, ref lpcb, ref lpcEntries);
            }

            if (apiResult != SUCCESS)
            {
               return null;
            }
            else
            {
               arrByte = new Byte[lpcb];

               Marshal.Copy(p, arrByte, 0, (int)lpcb);
               Marshal.FreeHGlobal(p);
               p = IntPtr.Zero;

               string[] result = new string[lpcEntries];

               for (int i = 0; i < (int)lpcEntries; i++)
               {
                  p = Marshal.AllocHGlobal(strSize);
                  Marshal.Copy(arrByte, (strSize * i), p, (int)strSize);

                  lprasentryname[i] = (RASENTRYNAME)Marshal.PtrToStructure(p, strType);

                  result[i] = lprasentryname[i].szEntryName;

                  Marshal.FreeHGlobal(p);
                  p = IntPtr.Zero;
               }

               return result;
            }
         }
         finally
         {
            if (p != IntPtr.Zero)
               Marshal.FreeHGlobal(p);
            p = IntPtr.Zero;
         }
      }

      private static byte[] ConvertToByte(RASCONN[] lprasentryname)
      {
         Type strType = typeof(RASCONN);
         int strSize = Marshal.SizeOf(strType);

         byte[] arrByte = new byte[strSize * lprasentryname.Length];

         for (int i = 0; i < lprasentryname.Length; i++)
         {
            IntPtr p = Marshal.AllocHGlobal(strSize);

            try
            {
               Marshal.StructureToPtr(lprasentryname[i], p, false);
               Marshal.Copy(p, arrByte, strSize * i, strSize);
            }
            finally
            {
               Marshal.FreeHGlobal(p);
            }
         }

         return arrByte;
      }

      private static byte[] ConvertToByte(RASENTRYNAME[] lprasentryname)
      {
         Type strType = typeof(RASENTRYNAME);
         int strSize = Marshal.SizeOf(strType);

         byte[] arrByte = new byte[strSize * lprasentryname.Length];

         for (int i = 0; i < lprasentryname.Length; i++)
         {
            IntPtr p = Marshal.AllocHGlobal(strSize);

            try
            {
               Marshal.StructureToPtr(lprasentryname[i], p, false);
               Marshal.Copy(p, arrByte, strSize * i, strSize);
            }
            finally
            {
               Marshal.FreeHGlobal(p);
            }
         }

         return arrByte;
      }
   }
}

 綺麗に整形する気もないので、このままで。
 このコードはたぶん、難しいです。マネージとアンマネージの間を行き来するので、わけわからんくなります。わけわからんと思った人は、素直に上司に「RASでは無理です」と言い切ってください。潔いとは大事な事です。

 そも、WindowsMobileは中では、RASで繋いでないしね。

 でも、世の常で、「ユーザーにはダイヤルアップの選択なんて出来るわけないだろー」と言われます。
 つーか、W-ZERO3でアプリ作るなら、メールやネットはWillcome使って、アプリケーションのデータは専用のサービスを使いたいっしょ。つーか、使うでしょ。Willcomeもそういうプラン出してるんだし。(企業向けにPHS網から入れるルータを出してる)

 つーわけで、作るしかないわけです。
 ここで、ConnectionManagerで唯一僕らが彼らに出る接続先変更の手だてが、「使い道がよくわからなかった、ConnMgrMapURL」です。

 こいつは何をするのかというと、要するに、指定されたURLを繋ぐのは、何を使うの?と、WindowsMoblieに問い合わせるものです。
 つまり、ここで、「これを使うのー!」が出来ればいい訳です。
 で、「これを使うのー!」をどこで設定するのかというと…

 画面上からは出来ません。

 ちょっと、MicrosoftのWindows CEの開発者を殴ってきていいですかね?

 文化の違いかわかりませんけどね。
 向こうでは、「Pocket PCを使う人なら、接続先を自分で変更するくらい、わけないさ、HAHAHAー!」なんですかね。

 いいなぁ…

 まぁ、でも日本ではそうではないわけで、こちらで選んであげなければいけません。どうやって?画面上からは出来ないのに?

 これ、より正確には、画面から出来ないのではなくて、画面から出来るのは、イントラネットへの例外のみなんですね。「そんなのあったな」という人は、たぶんこの問題にかなり深く関わってきている人だと思います。設定の、接続の詳細シートにあります。

 で、要するに、この部分の例外を「インターネット」にして、「ダイヤルアップ」に関連づけてあげればいい訳です。
 どうやって?
 まぁ、その話なんですけど。

 W-ZERO3って、オンラインサインアップ機能、ありますよね?
 あれで、「センタ名称設定」というのが出来ます。あれって、「なんであんな作業がいるのか」考えた事ってありますか?

 あれは僕、思うに、Microsoftの人か、SHARPの人が、サンプルコードを書いたんですよ。
 で、「センタ名称設定」っていうのは、Willcomeの人に「ここにセンタ名称設定設定してくださいね」っていう意味でやったんだと思うんです。で、Willcomeの人(SHARPかも)が、直し忘れたんだと思うんですよ。

 つまり、プログラムから、あれ、作る事が出来るんです。何で?
 C#で言うなら、Microsoft.WindowsMobile.Configuration.ConfigurationManagerクラスで。C/C++で言うなら、DMProcessConfigXMLというAPIです。

 これは何をするのかというと、XMLで端末の情報を取得したり、設定したり出来るAPIです。
 つまり、W-ZERO3のオンラインサインアップは、XMLでe-Mailの設定、および「センタ名称設定」の接続情報の設定をしているのです。だから、再起動が必要になりますし、XMLの直し忘れで(たぶん)「センタ名称設定」なのです!

 まぁ、どうでもいいですな、そんなことは。
 と言うわけで、ConnMgrMapURLで、任意の接続先に行ってもらうために、XMLで端末にその接続情報を設定します。
 俺的、サインアップ。

 ちゅーか、このDMProcessConfigXMLのXMLはすっげーわかりにくい、しかもドキュメントが英語しかないので、ちょっと難儀ですー。

//設定する
XmlDocument configDoc = new XmlDocument();
configDoc.LoadXml(
"<wap-provisioningdoc>" +
"<characteristic type=\"CM_Networks\">" +
   "<characteristic type=\"" + this.lblISPName.Text + "\">" +
   "<parm name=\"DestId\" value=\"{64243BC3-2658-4364-DFA3-D60BC135CB2C}\" />" +
   "<parm name=\"Secure\" value=\"0\" />" +
   "</characteristic>" +
"</characteristic>" +
"</wap-provisioningdoc>");

ConfigurationManager.ProcessConfiguration(configDoc, false);

configDoc = new XmlDocument();
configDoc.LoadXml(
"<wap-provisioningdoc>" +
"<characteristic type=\"CM_PPPEntries\">" +
   "<characteristic type=\"" + this.lblISPName.Text + "の接続\">" +
   "<parm name=\"DestId\" value=\"{64243BC3-2658-4364-DFA3-D60BC135CB2C}\" />" +
   "<parm name=\"Phone\" value=\"" + this.lblDialUpNo.Text + "\" />" +
   "<parm name=\"UserName\" value=\"" + this.lblUsername.Text + "\" />" +
   "<parm name=\"Password\" value=\"" + this.lblPassword.Text + "\" />" +
   "<parm name=\"Domain\" value=\"\" />" +
   "<parm name=\"DeviceType\" value=\"modem\" />" +
   "<parm name=\"DeviceName\" value=\"W-SIM\" />" +
   "</characteristic>" +
"</characteristic>" +
"</wap-provisioningdoc>");
ConfigurationManager.ProcessConfiguration(configDoc, false);

configDoc = new XmlDocument();
configDoc.LoadXml(
"<wap-provisioningdoc>" +
"<characteristic type=\"CM_Mappings\">" +
   "<characteristic type=\"1112\">" +
   "<parm name=\"Pattern\" value=\"" + this.lblMapUri.Text + "\" />" +
   "<parm name=\"Network\" value=\"{64243BC3-2658-4364-DFA3-D60BC135CB2C}\" />" +
   "</characteristic>" +
"</characteristic>" +
"</wap-provisioningdoc>");
ConfigurationManager.ProcessConfiguration(configDoc, false);

 とまぁ、このようにするとですよ。
 lblISPNameとゆー、まぁ、表示名のラベルなんですけどね。その接続先が、あの接続のドロップダウンに出てきます。(W-ZERO3ではこの名前が、「センタ名称設定」になってるわけ)
 で、その下の、〜の接続というのが、その接続に利用するモデムの設定。(W-ZERO3では、CLUB AIR-EDGE)
 lblDialUpNoは、電話番号。まぁ、テストで動かすなら、CLUB AIR-EDGEの番号でいいでしょう。lblUsername、lblPasswordはユーザ名とパスワードです。これは調べればたぶんすぐにわかると思うので、書かないけど。
 ってわけで、パスワードはここで設定した後、端末からは抜けなくなります。queryで呼び出しても、*になっちゃう。なので、セキュリティ的には問題ないと思います。設定しないのも出来るし。

 あとはまぁ、CM_Mappingsの1112という数字くらいですか。
 これは優先順位です。1000以下あたりは、予約されているそうなので、1000上で。デフォルトでは何も入っていないのでなんでもいいんでしょうけど、衝突しない程度に。
 で、lblMapUriで、例外とするURLを指定します。まぁ、ウチのサイトにするなら、*://www.studio-odyssey.net/*とすればいいです。*は何でもありになるので、これでhttpとhttpsとかですな。あと、サブディレクトリ用の/*。

 それぞれのリンクの関係は、GUIDを検索して見てください。ちゃんとそれぞれが繋がるようになっています。まぁ、GUIDはちゃんと取り直した方がいいと思いますけどね。

 こんな感じのアプリを作って、セットアップとして実行します。
 で、端末を再起動します。(リセットのコードとかはその辺に転がってるだろ)

 すると、接続先にこれが出てきます。例外は、画面から確認出来ませんが、設定されているはずです。試しに、http://www.studo-odyssey.net/にアクセスします。(例外をこれにしているなら)

 すると、作成した接続で設定されるはずです。
 されなかったら、何か設定間違っていると思うので、チェックしてください。DMProcessConfigXMLのサンプルコードは下にあります。

 接続が正しく出来たら、切断して、今度はgoogleあたりにつなげてみます。つーか、なんでもいいんですけど。
 すると、W-ZERO3なら、CLUB AIR-EDGEで繋ぎに行くはずです。(無線で繋ぎましたとか言った奴はぶっ殺す)

 とまぁ、そんな感じで、任意の接続先に、ConnectionManager APIで接続出来るわけです。

 あとはConnection ManagerのAPI周りは前回の日誌にある通りです。(注:なので、間違いというのは、接続時にurlがいるということ)

 2日かかりました。ここまで来るのに…

 Microsoftは、もうちょっとヘルプを充実させるべきだと思う。
 あと、日本語訳もがんばってくれ…いや、英語でもいいんだけどさ…

 DMProcessConfigXML APIの中身をさがしっこするためのサンプルコード(正確にはXML)です。これをAPIに渡すと、いろいろ出てくるよ!(C#では戻り値のXMLが中身の値です)
 それから、XMLの中に1つも-queryってなってないのは、「設定します」ので、注意。
 消す時は、no〜で消せます。この辺りはヘルプにもあるけど、英語です。注意。(まぁ、PDAなんてぶっ壊れたらリセットすればいいんだし)

//設定されている例外のリストを取得します。
//例外のリストを消すには、noparamがサポートされていないので(ヘルプみれ)、nocharacteristicで消す事になります。つまり、優先順位がtypeであり、それは1つだけとなります。
<wap-provisioningdoc><characteristic-query type=\"CM_Mappings\"/></wap-provisioningdoc>

//モデムリストを取得します。
//これを削除することもできますが、画面から消した方がいいです。このリストはモデムの一覧なので、接続先のリストではないです。
//接続先のリストを消去すると、これだけ残る事があります。手動で消すにはnocharacteristic。
<wap-provisioningdoc><characteristic-query type=\"CM_PPPEntries\"/></wap-provisioningdoc>

//接続先のリストを取得します。
//これを削除する事も出来ますが、画面から消した方がいいです。このリストは接続先のリストなので、モデムのリストではないです。
//センタ名称設定はここに入っています。消すには、nocharacteristicです。
<wap-provisioningdoc><characteristic-query type=\"CM_Networks\"/></wap-provisioningdoc>

 あなたのWindowsMobile開発がいいものになりますように…
 つーか、何か作ったら、是非ともコードの一部でも、公開して欲しいですよ。仕事でなければね。
 僕はGNUとかのコピーレフトはキライなんですけど。(BSDとかもあんまり好きではない)

 コードの一部はGNUにかからないとかそういうのでもいいじゃんさー。


2006.12.10

Windows Mobile Connection Manager の完全なコード

手動トラックバック先URI
http://www.studio-odyssey.net/content/note/archive01.htm#l1006

 と、言うわけで、前回書いた、Windows MobileのConnection Managerの完全なコードを乗せちゃうぞー!
 ペイが発生するものだけど、W-ZERO3を使ったアプリケーションが今後もっと出てきて欲しいから、乗せちゃうぞー!

 このコードは動くコードですが、完全に信頼出来るコードではないです。注意してください。保守とかはちょっと出来ないので、ご利用は自己責任でね!
 「ここはこうした方が」とか、「ここは問題でるよ」とか、「この方がかっくいいぜ!」とかあれば、BBS - みんなのケイジバンContact usからお願いします。
 つーか、是非是非、お願いします。
 マルチスレッドプログラミングとか、わからんねん。

 キーワードは、Windows Mobile、Windows CE、W-ZERO3、ネットワーク接続、Connection Manager APIとか、そのあたりかしら。
 とりあえず、土台固めからいきましょう。まずは、Connection Mangaer APIの、ConnMgrEstablishConnection で利用される、CONNMGR_CONNECTIONINFO構造体をC#で使えるようにします。
 僕はこれを1つのファイルとして作りました。Flagsの値はenumにした方がいいんだろうけど、めどいので、constにしました。あんまり外からは使わないしね。

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;

namespace BCL.WindowsMobile.Connection
{
   /// <summary>
   /// ConnectionInfo 構造体は、コネクション作成時の情報を格納する構造体です。
   /// </summary>
   /// <remarks>
   /// <para>
   /// この構造体は、Windows Mobile API で言うところの、CONNMGR_CONNECTIONINFO になります。
   /// 詳細はSDKヘルプを参照してください。
   /// </para>
   /// </remarks>
   [StructLayout(LayoutKind.Sequential)]
   public struct ConnectionInfo
   {
      /// <summary>
      /// 構造体のサイズを取得、または設定します。
      /// </summary>
      /// <remarks>
      /// <para>
      /// ぶっちゃけ、<see cref="Marshal.SizeOf(object)">Marshal.SizeOf</see> したものをいれるだけなんだけど。
      /// </para>
      /// </remarks>
      public uint Size;

      /// <summary>
      /// オプションとして機能させるフラグを指定します。このフィールドには、<see cref="ConnectionInfoParamsFlags"/> の値のマスクか、0を指定します。
      /// </summary>
      public uint Params;

      /// <summary>
      /// サポートされる接続のタイプを指定します。このフィールドには、<see cref="ConnectionInfoFlagsFlags"/> の値のマスクか、0を指定します。
      /// </summary>
      public uint Flags;

      /// <summary>
      /// 接続の優先順位を指定します。このフィールドには、 <see cref="ConnectionInfoPriorityFlags"/> のいずれかを指定します。
      /// </summary>
      public uint Priority;

      /// <summary>
      /// このフィールドを <c>true</c> にすると、この接続は他のアプリケーションから利用されなくなります。既定値は <c>false</c> です。
      /// </summary>
      /// <remarks>
      /// <para>
      /// If TRUE, this connection cannot be shared with other applications, no other applications are notified, and any application requesting a connection to the same network is treated as a contender for the same resource and is not permitted to share the existing connection. A decision is made between this connection and the others based on connection priority. If FALSE, the connection is shared among all applications and other applications with an interest in a connection to this network are notified that the connection is available.
      /// </para>
      /// </remarks>
      public int Exclusive;

      /// <summary>
      /// このフィールドを <c>true</c> にすると、ConnectionManagerは実際の接続を行いません。この接続が接続可能であれば、その状態を、ConnMgrConnectionStatus から取得する事が出来ます。規定値は <c>false</c> です。
      /// </summary>
      /// <remarks>
      /// <para>
      /// If TRUE, Connection Manager determines whether a connection can be made, although the ability to connect is disabled. When Connection Manager reaches the point when it would normally establish a connection, it sets the connection status to CONNMGR_STATUS_CONNECTIONDISABLED.
      /// </para>
      /// </remarks>
      public int Disabled;

      /// <summary>
      /// 接続が確立されるネットワークのGUID。接続先を指定してコネクションを作成するには、このフィールドを変更します。
      /// </summary>
      public Guid GuidDestNet;

      /// <summary>
      /// ConnectionManagerが状態変化メッセージを送信する、任意のウィンドウハンドル。0の時、Connectionマネージャはステータスメッセージを送信しません。
      /// </summary>
      public IntPtr hWnd;

      /// <summary>
      /// ConnectionManagerが状態変化メッセージをウィンドウに送信する際のメッセージID。
      /// </summary>
      public uint Msg;

      /// <summary>
      /// ConnectionManagerが状態変化メッセージをウィンドウに送信する際のlParam。
      /// </summary>
      public IntPtr lParam;

      /// <summary>
      /// 許可される最大接続のコスト。CONNMGR_PARAM_MAXCOST(0x2) です。
      /// </summary>
      public uint MaxCost;

      /// <summary>
      /// 許可される最小の許容できるレセプション帯域幅。CONNMGR_PARAM_MINRCVBW(0x4)です。
      /// </summary>
      public uint MinRcvBw;

      /// <summary>
      /// 許可される最大のレイテンシー。 CONNMGR_PARAM_MAXCONNLATENCY(0x8)です。
      /// </summary>
      public uint MaxConnLatency;
   }

   /// <summary>
   /// <see cref="ConnectionInfo.Params"/> に指定できるFlagのリストです。
   /// </summary>
   public static class ConnectionInfoParamsFlags
   {
      /// <summary>
      /// <see cref="ConnectionInfo.GuidDestNet"/> を有効にします。
      /// </summary>
      public const int CONNMGR_PARAM_GUIDDESTNET = 0x1;

      /// <summary>
      /// <see cref="ConnectionInfo.MaxCost"/> を有効にします。
      /// </summary>
      public const int CONNMGR_PARAM_MAXCOST = 0x2;

      /// <summary>
      /// <see cref="ConnectionInfo.MinRcvBw"/> を有効にします。
      /// </summary>
      public const int CONNMGR_PARAM_MINRCVBW = 0x4;

      /// <summary>
      /// <see cref="ConnectionInfo.MaxConnLatency"/> を有効にします。
      /// </summary>
      public const int CONNMGR_PARAM_MAXCONNLATENCY = 0x8; //constdefine MaxConnLatency field is valid
   }

   /// <summary>
   /// <see cref="ConnectionInfo.Flags"/> に指定できるFlagのリストです。
   /// </summary>
   public static class ConnectionInfoFlagsFlags
   {
      /// <summary>
      /// HTTP proxy
      /// </summary>
      public const int CONNMGR_FLAG_PROXY_HTTP = 0x1;

      /// <summary>
      /// WAP proxy (gateway)
      /// </summary>
      public const int CONNMGR_FLAG_PROXY_WAP = 0x2;

      /// <summary>
      /// SOCKS4 proxy supported.
      /// </summary>
      public const int CONNMGR_FLAG_PROXY_SOCKS4 = 0x4;

      /// <summary>
      /// SOCKS5 proxy supported.
      /// </summary>
      public const int CONNMGR_FLAG_PROXY_SOCKS5 = 0x8;
   }


   /// <summary>
   /// <see cref="ConnectionInfo.Priority"/> に指定できるFlagsのリストです。
   /// </summary>
   public static class ConnectionInfoPriorityFlags
   {
      /// <summary>
      /// Voice connection; highest priority.
      /// </summary>
      public const int CONNMGR_PRIORITY_VOICE = 0x20000;

      /// <summary>
      /// A user-initiated action caused this request, and the user interface is waiting on the creation of this connection.
      /// This flag is appropriate for an interactive browsing session or if the user selects "MORE" at the bottom of a truncated mail message (for example).
      /// </summary>
      public const int CONNMGR_PRIORITY_USERINTERACTIVE = 0x08000;

      /// <summary>
      /// A user-initiated connection that has recently become idle.
      /// A connection should be marked as idle when it is no longer the user's current task.
      /// </summary>
      public const int CONNMGR_PRIORITY_USERBACKGROUND = 0x02000;

      /// <summary>
      /// Interactive user task that has been idle for an application-specified amount of time.
      /// As the user runs the application, to help Connection Manager optimize responsiveness to the interactive application while sharing the connection with background applications, the application should toggle the state between CONNMGR_PRIORITY_USERIDLE and CONNMGR_PRIORITY_USERINTERACTIVE.
      /// </summary>
      public const int CONNMGR_PRIORITY_USERIDLE = 0x00800;

      /// <summary>
      /// High priority background connection.
      /// </summary>
      public const int CONNMGR_PRIORITY_HIPRIBKGND = 0x00200;
      
      /// <summary>
      /// Idle priority background connection.
      /// </summary>
      public const int CONNMGR_PRIORITY_IDLEBKGND = 0x00080;
      
      /// <summary>
      /// The connection is requested on behalf of an external entity but is an interactive session (AT Command Interpreter, for example).
      /// </summary>
      public const int CONNMGR_PRIORITY_EXTERNALINTERACTIVE = 0x00020;
      
      /// <summary>
      /// The lowest priority connection.
      /// Connection occurs only if a higher priority client is already using the same path.
      /// </summary>
      public const int CONNMGR_PRIORITY_LOWBKGND = 0x00008;
   }
}

 やっべ、見にくい。
 まぁ、実際にちゃんとみたい人は、コピペしてエディタに貼り付けるなりして見るだろうから、いいけど。

 長くなっても、見にくくても、完全なコードを載せるのが俺のジャスティス。
 完全なコードでなければ、意味がないッ!

 続いて、前回の例では、接続先情報のリストを取得する時とかに使っていた、CONNMGR_DESTINATION_INFO構造体の入れ物、DestinationInfo構造体ですが、これはちょっと使いやすいように変更しました。実際、外に向かって公開するものをDestinationInfo構造体として、Collectionをサポートさせました。CONNMGR_DESTINATION_INFO構造体は、そのまま使う事はまずないですし、.net側で操作しやすいようにしました。あと、indexというメンバも足しておきます。

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;

namespace BCL.WindowsMobile.Connection
{
   /// <summary>
   /// ConnectionManagerで利用される、ネットワークの情報を保持した構造体。
   /// </summary>
   /// <remarks>
   /// <para>
   /// 外部に向けてはこの構造体で公開されるが、内部でAPIとお話するのはこれではない。
   /// </para>
   /// </remarks>
   public struct DestinationInfo
   {
      internal DestinationInfo(int inedex, ConnectionManager.CmgDestinationInfo cmgDestInfo)
      {
         this.Index = inedex;
         this.Guid = cmgDestInfo.Guid;
         this.Description = cmgDestInfo.Description;
      }

      /// <summary>
      /// ConnectionManagerに登録されているIndex。
      /// </summary>
      public int Index;

      /// <summary>
      /// 接続が確立されるネットワークのGUID。接続先を指定してコネクションを作成するには、このフィールドを変更します。
      /// </summary>
      public Guid Guid;
      
      /// <summary>
      /// ネットワークの設定の名前
      /// </summary>
      public string Description;
   }
}

 で、これのCollection。

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

namespace BCL.WindowsMobile.Connection
{
   /// <summary>
   /// DestinationInfo を保持するコレクション。
   /// </summary>
   [Serializable]
   public class DestinationInfoCollection : System.Collections.CollectionBase
   {
      /// <summary>
      /// 指定したインデックスにある要素を取得または設定します。
      /// C# では、このプロパティは DestinationInfoCollection クラスのインデクサになります。
      /// </summary>
      public DestinationInfo this[int index]
      {
         get
         {
            return (DestinationInfo)List[index];
         }
         set
         {
            List[index] = value;
         }
      }

      /// <summary>
      /// <see cref="DestinationInfo"/> を <see cref="DestinationInfoCollection"/> の末尾に追加します。
      /// </summary>
      /// <param name="value"><see cref="DestinationInfoCollection"/> の末尾に追加する <see cref="DestinationInfo"/> 。</param>
      /// <returns>新しい要素が挿入される位置の 0 から始まるインデックス。</returns>
      public int Add(DestinationInfo value)
      {
         return List.Add(value);
      }

      /// <summary>
      /// 指定した <see cref="DestinationInfo"/> オブジェクトのコレクション内のインデックスを取得します。
      /// </summary>
      /// <param name="value">検索するオブジェクト。</param>
      /// <returns>見つかった場合は、インデックス番号の最も小さい要素のインデックス番号。それ以外の場合は、 - 1。</returns>
      public int IndexOf(DestinationInfo value)
      {
         return List.IndexOf(value);
      }

      /// <summary>
      /// 指定した description と一致するDescriptionの持つ DestinationInfo のインデックスを取得します。
      /// </summary>
      /// <param name="description">検索するオブジェクト。</param>
      /// <returns>見つかった場合は、インデックス番号の最も小さい要素のインデックス番号。それ以外の場合は、 - 1。</returns>
      public int IndexOf(string description)
      {
         for (int i = 0; i < this.Count; i++)
         {
            if (this[i].Description.Trim() == description.Trim())
               return i;
         }
         return -1;
      }

      /// <summary>
      /// <see cref="DestinationInfo"/> を <see cref="DestinationInfoCollection"/> の指定した位置に挿入します。
      /// </summary>
      /// <param name="index">value が挿入される位置の 0 から始まるインデックス。</param>
      /// <param name="value">挿入する <see cref="DestinationInfo"/> 。</param>
      public void Insert(int index, DestinationInfo value)
      {
         List.Insert(index, value);
      }

      /// <summary>
      /// <see cref="DestinationInfoCollection"/> 内で最初に見つかった <see cref="DestinationInfo"/> を 削除します。
      /// </summary>
      /// <param name="value"><see cref="DestinationInfoCollection"/> から削除する <see cref="DestinationInfo"/> 。</param>
      public void Remove(DestinationInfo value)
      {
         List.Remove(value);
      }

      /// <summary>
      /// 指定した <see cref="DestinationInfo"/> が <see cref="DestinationInfoCollection"/> 内にあるかどうかを確認します。
      /// </summary>
      /// <param name="value"><see cref="DestinationInfoCollection"/> 内で検索する <see cref="DestinationInfo"/>。</param>
      /// <returns>value が <see cref="DestinationInfoCollection"/> に存在する場合は <c>true</c> 。それ以外の場合は <c>false</c> 。</returns>
      public bool Contains(DestinationInfo value)
      {
         // If value is not of type DestinationInfo, this will return false.
         return List.Contains(value);
      }

      #region override
      /// <summary>
      /// 挿入時に呼び出されるもの。一致しないと例外なげます。
      /// </summary>
      /// <param name="index">位置</param>
      /// <param name="value">DestinationInfo</param>
      protected override void OnInsert(int index, Object value)
      {
         if (value.GetType() != typeof(BCL.WindowsMobile.Connection.DestinationInfo))
            throw new ArgumentException("value must be of type BCL.WindowsMobile.Connection.DestinationInfo. (" + value.GetType().ToString() + ")", "value");
      }

      /// <summary>
      /// 削除時に呼び出されるもの。
      /// </summary>
      /// <param name="index">位置</param>
      /// <param name="value">DestinationInfo</param>
      protected override void OnRemove(int index, Object value)
      {
         if (value.GetType() != typeof(BCL.WindowsMobile.Connection.DestinationInfo))
            throw new ArgumentException("value must be of type BCL.WindowsMobile.Connection.DestinationInfo. (" + value.GetType().ToString() + ")", "value");
      }

      /// <summary>
      /// Set時に呼び出されるもの。
      /// </summary>
      /// <param name="index">位置</param>
      /// <param name="oldValue">古いの</param>
      /// <param name="newValue">新しいの</param>
      protected override void OnSet(int index, Object oldValue, Object newValue)
      {
         if (newValue.GetType() != typeof(BCL.WindowsMobile.Connection.DestinationInfo))
            throw new ArgumentException("newValue must be of type BCL.WindowsMobile.Connection.DestinationInfo. (" + newValue.GetType().ToString() + ")", "newValue");
      }

      /// <summary>
      /// OnValidate
      /// </summary>
      /// <param name="value">DestinationInfo</param>
      protected override void OnValidate(Object value)
      {
         if (value.GetType() != typeof(BCL.WindowsMobile.Connection.DestinationInfo))
            throw new ArgumentException("value must be of type BCL.WindowsMobile.Connection.DestinationInfo. (" + value.GetType().ToString() + ")");
      }
      #endregion
   }
   

}

 んで、一応、例外も用意しておきます。

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

namespace BCL.WindowsMobile.Connection
{
   /// <summary>
   /// 接続に失敗した時に送出される例外。
   /// </summary>
   [global::System.Serializable]
   public class ConnectionException : Exception
   {
      /// <summary>
      /// ConnectionException クラスの新しいインスタンスを生成します。
      /// </summary>
      public ConnectionException() { }

      /// <summary>
      /// 指定したエラー メッセージを使用して、ConnectionException クラスの新しいインスタンスを初期化します。
      /// </summary>
      /// <param name="message">エラーを説明するメッセージ。</param>
      public ConnectionException(string message) : base(message) { }

      /// <summary>
      /// 指定したエラー メッセージと、この例外の原因である内部例外への参照を使用して、ConnectionException クラスの新しいインスタンスを初期化します。
      /// </summary>
      /// <param name="message">エラーを説明するメッセージ。</param>
      /// <param name="inner">現在の例外の原因である例外。内部例外が指定されていない場合は、null 参照 (Visual Basic の場合は Nothing)。</param>
      public ConnectionException(string message, Exception inner) : base(message, inner) { }
   }
}

 しかし、上のコレクションもそうだけど、この例外もスニペットでさくっと作れてしまうから、2005は楽でいいなぁ。

 非同期接続するので、接続したよイベントも捕まえておきます。
 まぁ、実際、このコードは適当なので、ちゃんとマルチスレッドする時は、キャンセルイベントとかも捕まえないとダメなんだけど…

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

namespace BCL.WindowsMobile.Connection
{
   /// <summary>
   /// 接続時に呼ばれるイベントで使用されるイベント引数。
   /// </summary>
   public class ConnectionEventArgs : EventArgs
   {
      /// <summary>
      /// ConnectionEventArgs クラスの新しいインスタンスを生成します。
      /// </summary>
      public ConnectionEventArgs() : this(IntPtr.Zero, 0, false) { }

      /// <summary>
      /// ConnectionEventArgs クラスの新しいインスタンスを生成します。
      /// </summary>
      /// <param name="connectionHanble">接続されたConnectionのHandle。</param>
      /// <param name="index">実際に接続された、または接続のための候補とされた接続先のIndex。</param>
      /// <param name="isApplicationConnected">アプリケーションが接続を行ったかどうか。</param>
      public ConnectionEventArgs(IntPtr connectionHanble, int index, bool isApplicationConnected)
      {
         this._handle = connectionHanble;
         this._index = index;
         this._isApplicationConnected = isApplicationConnected;
      }

      private IntPtr _handle;

      /// <summary>
      /// 接続されたハンドルを取得します。
      /// </summary>
      public IntPtr ConnectionHandle
      {
         get { return _handle; }
      }

      private int _index;

      /// <summary>
      /// 実際に接続、または接続のための候補とされたIndexを取得します。
      /// </summary>
      public int Index
      {
         get { return _index; }
      }

      private bool _isApplicationConnected;

      /// <summary>
      /// ConnectionManager が実際に接続したかを取得します。
      /// </summary>
      /// <remarks>
      /// <para>
      /// これは常にtrueな気がする。
      /// </para>
      /// </remarks>
      public bool IsApplicationConnected
      {
         get { return _isApplicationConnected; }
      }
   }
}

 さて、では本体である、Connection Managerクラスです。長いです。でも安心してください。唯一の救いは、このコードが完全で、動くことで(すくなくとも僕のZERO3では、僕の予想通りに)、あとはあなたが日本人であれば、僕に日本語が通じる事です。
 わからないこと等があれば、BBSにでもどうぞ。
 つーか、これが正しいとは思わないですしね。不安の方が大きいわけで。

 ともあれ、出来る人なら、動くっぽいコードがあれば、自力でなんとか出来るでしょう。
 おーしーえーてー!(出来る人ならこんなトコみない)

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Threading;

namespace BCL.WindowsMobile.Connection
{
   /// <summary>
   /// ConnectionManager クラスの接続時に呼ばれる、<see cref="ConnectionManager.OnConnected"/> イベントのためのデリゲート
   /// </summary>
   /// <param name="sender">イベントを発生させたオブジェクト。</param>
   /// <param name="e">イベント引数</param>
   public delegate void OnConnectedEventHandler(object sender, ConnectionEventArgs e);

   /// <summary>
   /// ConnectionManager APIの操作をC#で行うためのラッパークラス。
   /// </summary>
   public sealed class ConnectionManager : IDisposable
   {
      #region Fields

      private IntPtr _connectionHandle;
      private bool _isApplicationConnected;
      private readonly int _timeout;
      private static object lockObject = new object();
      #endregion

      #region constructor
      /// <summary>
      /// ConnectionManager クラスの新しいインスタンスを生成します。
      /// </summary>
      public ConnectionManager() : this(0) { }

      /// <summary>
      /// ConnectionManager クラスの新しいインスタンスを生成します。
      /// </summary>
      /// <param name="timeout">接続時のタイムアウトまでのミリ秒</param>
      public ConnectionManager(int timeout)
      {
         _connectionHandle = IntPtr.Zero;
         _isApplicationConnected = false;
         _timeout = timeout;
      }
      #endregion

      #region Dispose / ConnectionManager によって使用されているすべてのリソースを解放します。
      /// <summary>
      /// ConnectionManager によって使用されているすべてのリソースを解放します。
      /// </summary>
      public void Dispose()
      {
         lock (lockObject)
         {
            HangupConnection();
         }
      }
      #endregion

      //DllImports
      #region ConnMgrEnumDestinations
      /// <summary>
      /// ConnectionManagerの設定を取得します。
      /// </summary>
      /// <param name="nIndex">取得するインテックス。</param>
      /// <param name="pDestinationInfo">取得するデータが返る、<see cref="DestinationInfo"/> 。</param>
      /// <remarks>
      /// <para>
      /// HRESULT WINAPI ConnMgrEnumDestinations(int nIndex, CONNMGR_DESTINATION_INFO * pDestInfo );
      /// </para>
      /// </remarks>
      [DllImport("cellcore.dll", EntryPoint = "ConnMgrEnumDestinations", SetLastError = true)]
      internal static extern int ConnMgrEnumDestinations(int nIndex, ref CmgDestinationInfo pDestinationInfo);
      #endregion

      #region ConnMgrEstablishConnection
      /// <summary>
      /// コネクションを作成します。
      /// </summary>
      /// <param name="pConnInfo">接続の情報を格納した、<see cref="ConnectionInfo"/> 。</param>
      /// <param name="phConnection">作成したコネクションへのハンドル。</param>
      /// <returns>
      /// <c>S_OK</c>
      /// The connection was successfully requested.
      /// Error code
      /// The connection could not be requested.
      /// </returns>
      /// <remarks>
      /// <para>
      /// HRESULT WINAPI ConnMgrEstablishConnection(CONNMGR_CONNECTIONINFO * pConnInfo, HANDLE * phConnection);
      /// </para>
      /// </remarks>
      [DllImport("cellcore.dll",EntryPoint="ConnMgrEstablishConnection",SetLastError=true)]
      internal static extern int ConnMgrEstablishConnection(ref ConnectionInfo pConnInfo, ref IntPtr phConnection);
      #endregion

      #region ConnMgrEstablishConnectionSync
      /// <summary>
      /// コネクションを同期モードで作成します。
      /// </summary>
      /// <param name="pConnInfo">接続の情報を格納した、<see cref="ConnectionInfo"/> 。</param>
      /// <param name="phConnection">作成したコネクションへのハンドル。</param>
      /// <param name="dwTimeout">タイムアウトまでのミリ秒</param>
      /// <param name="pdwStatus">最後のステータスを保持したデータ。CONNMGR_STATUS_* のいずれか。</param>
      /// <returns>
      /// None of the following values are returned until the connection either has been established or has failed.
      /// <c>S_OK</c>
      /// The connection was successfully requested.
      /// Error code
      /// The connection could not be requested.
      /// </returns>
      /// <remarks>
      /// <para>
      /// HRESULT WINAPI ConnMgrEstablishConnectionSync(CONNMGR_CONNECTIONINFO * pConnInfo, HANDLE * phConnection, DWORD dwTimeout, DWORD * pdwStatus);
      /// </para>
      /// </remarks>
      [DllImport("cellcore.dll",EntryPoint="ConnMgrEstablishConnectionSync",SetLastError=true)]
      internal static extern int ConnMgrEstablishConnectionSync(ref ConnectionInfo pConnInfo, ref IntPtr phConnection, uint dwTimeout, ref uint pdwStatus);
      #endregion

      #region ConnMgrReleaseConnection
      /// <summary>
      /// コネクションを解放します。
      /// </summary>
      /// <param name="hConnection">解放するハンドル。</param>
      /// <param name="ICache">切断までの秒数。すぐに切断するには0。1でデフォルトの60秒。</param>
      /// <returns>None</returns>
      /// <remarks>
      /// <para>
      /// HRESULT WINAPI ConnMgrReleaseConnection(HANDLE hConnection, LONG ICache );
      /// </para>
      /// </remarks>
      [DllImport("cellcore.dll", EntryPoint = "ConnMgrReleaseConnection", SetLastError = true)]
      internal static extern int ConnMgrReleaseConnection(IntPtr hConnection, int ICache);
      #endregion

      #region ConnMgrMapURL
      /// <summary>
      /// ConnectionManagerにURLのGUIDをマップします。
      /// </summary>
      /// <param name="pwszUrl">マップするUrl</param>
      /// <param name="pguid">接続先のGUID</param>
      /// <param name="pdwIndex">マッピングに利用する、ConnectionManagerのリストのIndex。</param>
      /// <returns>None</returns>
      /// <remarks>
      /// <para>
      /// HRESULT WINAPI ConnMgrMapURL(LPCTSTR pwszURL, GUID * pguid, DWORD * pdwIndex);
      /// </para>
      /// </remarks>
      [DllImport("cellcore.dll",EntryPoint="ConnMgrMapURL",SetLastError=true)]
      internal static extern int ConnMgrMapURL(string pwszUrl, ref Guid pguid, ref uint pdwIndex);
      #endregion

      #region ConnMgrConnectionStatus
      /// <summary>
      /// ConnectionManagerのステータスを取得します。
      /// </summary>
      /// <param name="hConnection">取得するハンドル。</param>
      /// <param name="pdwStatus">ステータスを保持したデータ。CONNMGR_STATUS_* のいずれか。</param>
      /// <returns>None.</returns>
      /// <remarks>
      /// <para>
      /// HRESULT WINAPI ConnMgrConnectionStatus(HANDLE hConnection, DWORD * pdwStatus );
      /// </para>
      /// </remarks>
      [DllImport("cellcore.dll", EntryPoint = "ConnMgrConnectionStatus", SetLastError = true)]
      internal static extern int ConnMgrConnectionStatus(IntPtr hConnection, ref uint pdwStatus);
      #endregion

      //STATUS const
      #region CONNMGR_STATUS_* / pdwStatus
      /// <summary>
      /// コネクションのステータス状態は不明。
      /// </summary>
      public const uint CONNMGR_STATUS_UNKNOWN = 0x00;

      /// <summary>
      /// 接続されている。
      /// </summary>
      public const uint CONNMGR_STATUS_CONNECTED = 0x10;

      /// <summary>
      /// 切断されている。
      /// </summary>
      public const uint CONNMGR_STATUS_DISCONNECTED = 0x20;

      /// <summary>
      /// 接続は失敗し、再接続出来ない。
      /// </summary>
      public const uint CONNMGR_STATUS_CONNECTIONFAILED = 0x21;

      /// <summary>
      /// ユーザーが接続を中止した。
      /// </summary>
      public const uint CONNMGR_STATUS_CONNECTIONCANCELED = 0x22;

      //もう、訳すのめどい
      /// <summary>
      /// The connection can be made, although the connection is disabled. This value is returned only to clients that set the bDisabled value in the CONNMGR_CONNECTIONINFO structure.
      /// </summary>
      public const uint CONNMGR_STATUS_CONNECTIONDISABLED = 0x23;

      /// <summary>
      /// No path to the destination could be found.
      /// </summary>
      public const uint CONNMGR_STATUS_NOPATHTODESTINATION = 0x24;

      /// <summary>
      /// A path to the destination exists but is not presently available (for example, the device is out of radio range or is not plugged into its cradle).
      /// </summary>
      public const uint CONNMGR_STATUS_WAITINGFORPATH = 0x25;

      /// <summary>
      /// An in-progress voice call is using resources that this connection requires.
      /// </summary>
      public const uint CONNMGR_STATUS_WAITINGFORPHONE = 0x26;

      /// <summary>
      /// The device is attempting to connect.
      /// </summary>
      public const uint CONNMGR_STATUS_WAITINGCONNECTION = 0x40;

      /// <summary>
      /// Another client is using resources that this connection requires.
      /// </summary>
      public const uint CONNMGR_STATUS_WAITINGFORRESOURCE = 0x41;

      /// <summary>
      /// The device is waiting for a task with a higher priority to connect to the network before connecting to the same network. This status value is returned only to clients that specify a priority of CONNMGR_PRIORITY_LOWBKGND when requesting a connection.
      /// </summary>
      public const uint CONNMGR_STATUS_WAITINGFORNETWORK = 0x42;

      /// <summary>
      /// The connection is being brought down.
      /// </summary>
      public const uint CONNMGR_STATUS_WAITINGDISCONNECTION = 0x80;

      /// <summary>
      /// The device is aborting the connection attempt.
      /// </summary>
      public const uint CONNMGR_STATUS_WAITINGCONNECTIONABORT = 0x81;
      #endregion

      #region struct CmgDestinationInfo
      private const int CONNMGR_MAX_DESC = 128;
      
      /// <summary>
      /// ConnectionManagerで利用される、ネットワークの情報を保持した構造体。
      /// </summary>
      [StructLayout(LayoutKind.Sequential)]
      internal struct CmgDestinationInfo
      {
         /// <summary>
         /// 接続が確立されるネットワークのGUID。接続先を指定してコネクションを作成するには、このフィールドを変更します。
         /// </summary>
         public Guid Guid;
         
         /// <summary>
         /// ネットワークの設定の名前
         /// </summary>
         /// <remarks>
         /// <para>
         /// SizeConst = CONNMGR_MAX_DESC
         /// </para>
         /// </remarks>
         [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CONNMGR_MAX_DESC)]
         public string Description;
      }
      #endregion

      #region GetConnectionSettings / ConnectionManagerに登録されている接続先の一覧を取得します。
      /// <summary>
      /// ConnectionManagerに登録されている接続先の一覧を取得します。
      /// </summary>
      /// <returns>接続先の一覧である <see cref="DestinationInfoCollection"/> 。この並びはindexである。</returns>
      public DestinationInfoCollection GetConnectionSettings()
      {
         DestinationInfoCollection result = new DestinationInfoCollection();

         for (int index = 0; index < Int32.MaxValue; index++)
         {
            CmgDestinationInfo dest = new CmgDestinationInfo();

            if (0 > ConnMgrEnumDestinations(index, ref dest))
               break;

            result.Add(new DestinationInfo(index, dest));
         }

         return result;
      }
      #endregion

      #region GetConnectionSetting / ConnectionManagerに登録されている、指定されたindexの接続先を取得します。
      /// <summary>
      /// ConnectionManagerに登録されている、指定されたindexの接続先を取得します。
      /// </summary>
      /// <param name="index">取得するindex。</param>
      /// <returns>接続先の <see cref="DestinationInfo"/> 。</returns>
      public DestinationInfo GetConnectionSetting(int index)
      {
         CmgDestinationInfo dest = new CmgDestinationInfo();

         if (ConnMgrEnumDestinations(index, ref dest) >= 0)
         {
            return new DestinationInfo(index, dest);
         }
         return new DestinationInfo();
      }
      #endregion

      #region GetNetworkFromPath / 接続用のGUIDを取得します。
      /// <summary>
      /// uriパスから、指定されたindexの接続用のGUIDをoutします。
      /// </summary>
      /// <param name="url">マップするUrl</param>
      /// <param name="guid">生成された、MobileGuid</param>
      /// <param name="index">マップに使用する接続先のindex</param>
      /// <returns>取得した接続用のGUID</returns>
      /// <exception cref="ConnectionException">取得に失敗した時にthrowされます。</exception>
      private int GetNetworkFromPath(string url, out Guid guid, ref uint index)
      {
         guid = new Guid();
         return ConnMgrMapURL(url, ref guid, ref index);
      }
      #endregion

      #region GetConnectionStatus / 指定されたハンドルの接続状態を取得します。取得される値は、CONNMGR_STATUS_* のいずれかです。
      /// <summary>
      /// 指定されたハンドルの接続状態を取得します。取得される値は、CONNMGR_STATUS_* のいずれかです。
      /// </summary>
      /// <param name="uri">接続を試みるUrl。</param>
      /// <returns>CONNMGR_STATUS_* のいずれか。</returns>
      /// <exception cref="ConnectionException">取得に失敗した時に送出される例外。</exception>
      public uint GetConnectionStatus(Uri uri)
      {
         return GetConnectionStatus(uri.AbsoluteUri);
      }

      /// <summary>
      /// 指定されたハンドルの接続状態を取得します。取得される値は、CONNMGR_STATUS_* のいずれかです。
      /// </summary>
      /// <param name="url">接続を試みるUrl</param>
      /// <returns>CONNMGR_STATUS_* のいずれか。</returns>
      /// <exception cref="ConnectionException">取得に失敗した時に送出される例外。</exception>
      public uint GetConnectionStatus(string url)
      {
         uint index = 0;
         ConnectionInfo connInfo = new ConnectionInfo();
         IntPtr connectionHandle = IntPtr.Zero;

         connInfo.Size = (uint)Marshal.SizeOf(connInfo);
         connInfo.Params = ConnectionInfoParamsFlags.CONNMGR_PARAM_GUIDDESTNET;
         connInfo.Flags = 0;
         connInfo.Disabled = 1;    //実際には繋がない
         connInfo.Priority = ConnectionInfoPriorityFlags.CONNMGR_PRIORITY_USERINTERACTIVE;
         
         if (0 > GetNetworkFromPath(url, out connInfo.GuidDestNet, ref index))
         {
            DoConnectingError();
         }

         uint currentStatus = CONNMGR_STATUS_UNKNOWN;

         int result = ConnMgrEstablishConnection(ref connInfo, ref connectionHandle);

         if (result >= 0)
         {
            currentStatus = GetConnectionStatus(connectionHandle);
         }
         return currentStatus;
      }

      /// <summary>
      /// 指定されたハンドルの接続状態を取得します。取得される値は、CONNMGR_STATUS_* のいずれかです。
      /// </summary>
      /// <param name="connectionHandle">取得する接続のハンドル。</param>
      /// <returns>CONNMGR_STATUS_* のいずれか。</returns>
      /// <exception cref="ConnectionException">取得に失敗した時に送出される例外。</exception>
      private uint GetConnectionStatus(IntPtr connectionHandle)
      {
         uint currentStatus = 0;
         if (0 > ConnMgrConnectionStatus(connectionHandle, ref currentStatus))
         {
            DoConnectingError();
         }
         return currentStatus;
      }
      #endregion

      #region HasConnection / コネクションを保持しているかを取得します。
      /// <summary>
      /// コネクションを保持しているかを取得します。
      /// </summary>
      /// <returns>保持していれば <c>true</c> 。それ以外は <c>false</c> 。</returns>
      public bool HasConnection()
      {
         return (this._connectionHandle != IntPtr.Zero);
      }
      #endregion

      #region EstablishConnection / 指定された index の接続先へ接続します。
      /// <summary>
      /// 指定された index の接続先へ接続します。
      /// </summary>
      /// <param name="url">接続するurl</param>
      public void EstablishConnection(string url)
      {
         lock (lockObject)
         {
            HangupConnection();
         }
         ConnectionThread thread = new ConnectionThread(this, url, _timeout);
      }
      #endregion

      /// <summary>
      /// このクラスが接続した時に呼ばれます。このクラスが接続した時だけなので、すでに繋がっている時等には呼ばれません。
      /// </summary>
      public event OnConnectedEventHandler OnConnected;

      //これは接続された時に呼び出されてくる
      #region EstablishConnection_CallBack / Methoed
      private void EstablishConnection_CallBack(object sender, ConnectionEventArgs e)
      {
         lock (lockObject)
         {
            this._connectionHandle = e.ConnectionHandle;
            this._isApplicationConnected = e.IsApplicationConnected;
         }
         if (OnConnected != null)
         {
            OnConnected(this, e);
         }
      }
      #endregion

      //非同期で接続するためのもの
      #region ConnectionThread / class
      private class ConnectionThread
      {
         private readonly ConnectionManager _parent;
         private readonly string _url;
         private readonly int _timeout;

         public ConnectionThread(ConnectionManager parent, string url, int timeout)
         {
            this._parent = parent;
            this._url = url;
            this._timeout = timeout;
            
            Thread thread = new Thread(new ThreadStart(Run));
            thread.IsBackground = true;
            thread.Priority = ThreadPriority.Highest;

            thread.Start();
         }

         private void Run()
         {
            IntPtr connectionHandle = IntPtr.Zero;
            uint index = 0;
            int result = 0;

            uint finalStatus = CONNMGR_STATUS_UNKNOWN;

            DateTime end = (this._timeout != 0) ? DateTime.Now.AddMilliseconds(this._timeout) : DateTime.MaxValue;

            do
            {
               ConnectionInfo connInfo = new ConnectionInfo();
               
               connInfo.Size = (uint)Marshal.SizeOf(connInfo);
               connInfo.Params = ConnectionInfoParamsFlags.CONNMGR_PARAM_GUIDDESTNET;
               connInfo.Flags = 0;
               connInfo.Disabled = 0;
               connInfo.Priority = ConnectionInfoPriorityFlags.CONNMGR_PRIORITY_USERINTERACTIVE;

               result = _parent.GetNetworkFromPath(_url, out connInfo.GuidDestNet, ref index);

               if (result != 0)
               {
                  finalStatus = CONNMGR_STATUS_CONNECTIONFAILED;
                  break;
               }

               result = ConnMgrEstablishConnectionSync(ref connInfo, ref connectionHandle, 150000, ref finalStatus);

            } while (finalStatus != CONNMGR_STATUS_CONNECTED && end > DateTime.Now);

            if (finalStatus == CONNMGR_STATUS_CONNECTED)
            {
               _parent.EstablishConnection_CallBack(this, new ConnectionEventArgs(connectionHandle, (int)index, true));
            }
         }
      }
      #endregion

      #region HangupConnection / このクラスを通して接続した接続を切断します。
      /// <summary>
      /// このクラスを通して接続した接続を切断します。
      /// </summary>
      public void HangupConnection()
      {
         if (this._isApplicationConnected && this._connectionHandle != IntPtr.Zero)
         {
            int result = ConnMgrReleaseConnection(_connectionHandle, 0);

            if (0 > result)
            {
               DoConnectingError();
            }
            _connectionHandle = IntPtr.Zero;
         }
      }
      #endregion

      #region DoConnectingError / 接続に失敗した時にthrowする例外を飛ばすためのメソッド。あんまり意味ない。
      /// <summary>
      /// 接続に失敗した時にthrowする例外を飛ばすためのメソッド。あんまり意味ない。
      /// </summary>
      internal void DoConnectingError()
      {
         throw new ConnectionException(String.Format("{0} - ErrorCode : {1}", Properties.Resources.ConnectionException, Marshal.GetLastWin32Error()));
      }
      #endregion
   }
}

 なげぇ。

 解説の前に、これを利用するFormのコードを先に提示します。
 使い方がわかれば、理解もしやすいと思うので。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace ConnectionTest
{
   public partial class Form1 : Form
   {
      BCL.WindowsMobile.Connection.ConnectionManager mgr;
      public Form1()
      {
         InitializeComponent();
         mgr = new BCL.WindowsMobile.Connection.ConnectionManager();
      }

      private void Form1_Load(object sender, EventArgs e)
      {
         //接続先一覧くれよー
         foreach (BCL.WindowsMobile.Connection.DestinationInfo info in mgr.GetConnectionSettings())
         {
            this.cmbConnectionList.Items.Add(info.Description);
         }
      }

      private void button1_Click(object sender, EventArgs e)
      {
         mgr.EstablishConnection("http://www.studio-odyssey.net");
      }

      private void button2_Click(object sender, EventArgs e)
      {
         mgr.HangupConnection();
      }

      private void button3_Click(object sender, EventArgs e)
      {
         if (mgr.HasConnection())
            MessageBox.Show("true");
         else
            MessageBox.Show("false");
      }
   }
}

 いじょ。

 コンストラクタでConnectionManagerを実体化して、持っておきます。
 Loadイベントで、GetConnectionSettingsを使って接続先の一覧を取得して、ComboBoxに突っ込みます。ただのDropDownListです。ただ、これにはそんなに意味はなくて、ただ取れるよというだけです。
 で、button1_Clickのイベントで選択されているところに繋ぎに行きます。
 button2_Clickは切断です。button3_Clickは接続されているかを取得します。

 このConnectionManagerは仕様上、1つの接続しかもてないので、HasConnectionメソッドはいちいちConnectionManager APIに問い合わせている訳ではなくて、ハンドルを持っているかどうかだけで判断しています。どうかと思うけどね、まぁ、いっかなーと。

 ConnectionManagerはDisposableなので、Formの破棄と共にConnectionも破棄されます。これも仕様。僕が使う上では、Connectionをつなぎっぱなしって言うことはしないので、そうなってます。

 んじゃ、ConnectionManagerクラスを駆け足で見ていきます。XMLコメントはうざいので、消してしまいます。さくさくと見ていきましょう。

private IntPtr _connectionHandle;
private static object lockObject = new object();

 ここがConnectionのハンドルを保持するところです。外には見えないです。
 一応、lockを使ってマルチスレッドっぽくしてありますが、僕はマルチスレッドプログラミング詳しくないので、似非です。本当にちゃんとなっているかどうかは、知らないです。わかる人は直した方がいいです。教えてください。

public ConnectionManager(int timeout)
{
   _connectionHandle = IntPtr.Zero;
   _isApplicationConnected = false;
   _timeout = timeout;
}

 コンストラクタで初期化しています。
 コンストラクタでなくて、普通に初期化してもいいじゃーん。
 なんとなくです。

 タイムアウトの処理は適当です。0でタイムアウトしないです。ミリ秒。
 ここはオーパヘッドあると思うんですけど、まぁ、めどいのでいっかなーと。(そんなに僕が相手にするところはオーダーがきつくないので)

public void Dispose()
{
   lock (lockObject)
   {
      HangupConnection();
   }
}

 Disposableなんですけど、これでいいのかは知りません。
 lockって、ここでもつかえんの?
 つーか、tryした方がいいのか?

[DllImport("cellcore.dll", EntryPoint = "ConnMgrEnumDestinations", SetLastError = true)]
internal static extern int ConnMgrEnumDestinations(int nIndex, ref CmgDestinationInfo pDestinationInfo);

[DllImport("cellcore.dll",EntryPoint="ConnMgrEstablishConnection",SetLastError=true)]
internal static extern int ConnMgrEstablishConnection(ref ConnectionInfo pConnInfo, ref IntPtr phConnection);

[DllImport("cellcore.dll",EntryPoint="ConnMgrEstablishConnectionSync",SetLastError=true)]
internal static extern int ConnMgrEstablishConnectionSync(ref ConnectionInfo pConnInfo, ref IntPtr phConnection, uint dwTimeout, ref uint pdwStatus);

[DllImport("cellcore.dll", EntryPoint = "ConnMgrReleaseConnection", SetLastError = true)]
internal static extern int ConnMgrReleaseConnection(IntPtr hConnection, int ICache);

[DllImport("cellcore.dll",EntryPoint="ConnMgrMapURL",SetLastError=true)]
internal static extern int ConnMgrMapURL(string pwszUrl, ref Guid pguid, ref uint pdwIndex);

[DllImport("cellcore.dll", EntryPoint = "ConnMgrConnectionStatus", SetLastError = true)]
internal static extern int ConnMgrConnectionStatus(IntPtr hConnection, ref uint pdwStatus);

 Connection Manager APIのDllImportです。使ってないのもあります。一応、用意するだけしてあります。
 このあたりはSDKヘルプにある通りだから、別段難しくはないんだよなー。

private const int CONNMGR_MAX_DESC = 128;

[StructLayout(LayoutKind.Sequential)]
internal struct CmgDestinationInfo
{
   public Guid Guid;

   [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CONNMGR_MAX_DESC)]
   public string Description;
}

 接続先の一覧を取得する時のいれものです。外に出す時には、Collectionに突っ込んでから使っています。別にそのままでもいいと思うんですけど、indexがあった方がいいのかなぁと思ったので、こうしてあります。indexの使い道は、ないんですけどね。

public DestinationInfoCollection GetConnectionSettings()
{
   DestinationInfoCollection result = new DestinationInfoCollection();

   for (int index = 0; index < Int32.MaxValue; index++)
   {
      CmgDestinationInfo dest = new CmgDestinationInfo();

      if (0 > ConnMgrEnumDestinations(index, ref dest))
         break;

      result.Add(new DestinationInfo(index, dest));
   }

   return result;
}

 接続先の一覧を取得して、Collectionに突っ込んで返します。

 GetNetworkFromPath(string url, out Guid guid, ref uint index) は、指定されたurlを、Guidにマップします。この時の動作が、ConnectionManagerでのキモなのですが、とりあえずここでは置いておきます。
 とまれ、これで接続の準備はできます。

 private uint GetConnectionStatus(IntPtr connectionHandle)も、特に特筆する事はないです。ステータスもらうだけです。

 戻り値はCONNMGR_STATUS_*のいずれかです。

 public bool HasConnection()も特に特筆することはないです。

public void EstablishConnection(string url)
{
   lock (lockObject)
   {
      HangupConnection();
   }
   ConnectionThread thread = new ConnectionThread(this, url, _timeout);
}

 接続する時に呼ばれるものです。新しいクラスを作って終わりです。問題はこの、ConnectionThreadクラスですが、compactframeworkがdelegateのAsyncをサポートしていないので(書けるしコンパイルエラーにもならないが、動かないというすてき仕様)、クラスのコンストラクタを使って、別のスレッドを走らせています。

private class ConnectionThread
{
   private readonly ConnectionManager _parent;
   private readonly string _url;
   private readonly int _timeout;

   public ConnectionThread(ConnectionManager parent, string url, int timeout)
   {
      this._parent = parent;
      this._url = url;
      this._timeout = timeout;
      
      Thread thread = new Thread(new ThreadStart(Run));
      thread.IsBackground = true;
      thread.Priority = ThreadPriority.Highest;

      thread.Start();
   }

 こんな感じに。

private void Run()
{
   IntPtr connectionHandle = IntPtr.Zero;
   uint index = 0;
   int result = 0;

   uint finalStatus = CONNMGR_STATUS_UNKNOWN;

   DateTime end = (this._timeout != 0) ? DateTime.Now.AddMilliseconds(this._timeout) : DateTime.MaxValue;

   do
   {
      ConnectionInfo connInfo = new ConnectionInfo();
      
      connInfo.Size = (uint)Marshal.SizeOf(connInfo);
      connInfo.Params = ConnectionInfoParamsFlags.CONNMGR_PARAM_GUIDDESTNET;
      connInfo.Flags = 0;
      connInfo.Disabled = 0;
      connInfo.Priority = ConnectionInfoPriorityFlags.CONNMGR_PRIORITY_USERINTERACTIVE;

      result = _parent.GetNetworkFromPath(_url, out connInfo.GuidDestNet, ref index);

      if (result != 0)
      {
         finalStatus = CONNMGR_STATUS_CONNECTIONFAILED;
         break;
      }

      result = ConnMgrEstablishConnectionSync(ref connInfo, ref connectionHandle, 150000, ref finalStatus);

   } while (finalStatus != CONNMGR_STATUS_CONNECTED && end > DateTime.Now);

   if (finalStatus == CONNMGR_STATUS_CONNECTED)
   {
      _parent.EstablishConnection_CallBack(this, new ConnectionEventArgs(connectionHandle, (int)index, true));
   }
}

 で、これが接続の部分。
 スレッドで走らせて、うりうりとする。
 ただ、ここwhileのループなんで、一応、それっぽくタイムアウトさせる。めどいのでいい加減だし、チェックしてないけど。
 このためだけにAPI呼ぶのもばからしいしなぁ。

 接続した後、EstablishConnection_CallBackが呼ばれます。これスレッドまたぐのでどうかと思うけど、まぁ、とりあえず受け取らないとわかんないので。

private void EstablishConnection_CallBack(object sender, ConnectionEventArgs e)
{
   lock (lockObject)
   {
      this._connectionHandle = e.ConnectionHandle;
      this._isApplicationConnected = e.IsApplicationConnected;
   }
   if (OnConnected != null)
   {
      OnConnected(this, e);
   }
}

 あとは切断のHangupConnectionと、例外飛ばすためのDoConnectingErrorだけなので割愛。

 なんだかんだ言って、このレベルのものを作るのにも、半日以上かかってしまった…

 Guid型が、.netの型だとおかしくなるという話をしていたものを、修正しました。問題ないようです。(違う問題だったらしい)
 おこやえゆー!

 単にコーディング上の問題だったようで…つーか、ここまで来ると、ただのC#への移植でんな。
 C++のサンプルを焼き直した方が早くね?


2006.10.11

コードスニペットその2

手動トラックバック先URI
http://www.studio-odyssey.net/content/note/archive01.htm#j1106

 そんなわけで、今回のコードスニペットは、CollectionBaseの派生を作るスニペット。

 コレクションクラスがわずかタブ数回と、キーボード数十回に!?

 つーか、List<T>でよくね?

<?xml version="1.0" encoding="utf-8" ?>
   <CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
      <CodeSnippet Format="1.0.0">
         <Header>
            <Title>CollectionBaseSnippet</Title>
            <Shortcut>colba</Shortcut>
            <Description>CollectionBaseを継承した、コレクションクラスを定義するためのスニペット。</Description>
            <Author>studio Odyssey</Author>
            <SnippetTypes>
               <SnippetType>Expansion</SnippetType>
            </SnippetTypes>
         </Header>
         <Snippet>
            <Declarations>
               <Literal>
                  <ID>CollectionTargetClass</ID>
                  <ToolTip>コレクションの元となるクラス名</ToolTip>
                  <Default>Int16</Default>
               </Literal>
               <Literal>
                  <ID>System</ID>
                  <ToolTip>namespace</ToolTip>
                  <Default>System</Default>
               </Literal>
            </Declarations>
            <Code Language="csharp">
            <![CDATA[
[Serializable]
public class $CollectionTargetClass$Collection : System.Collections.CollectionBase
{
   /// <summary>
   /// 指定したインデックスにある要素を取得または設定します。
   /// C# では、このプロパティは $CollectionTargetClass$Collection クラスのインデクサになります。
   /// </summary>
   public $CollectionTargetClass$ this[int index]
   {
      get
      {
         return ($CollectionTargetClass$)List[index];
      }
      set
      {
         List[index] = value;
      }
   }
   
   /// <summary>
   /// <see cref="$CollectionTargetClass$"/> を <see cref="$CollectionTargetClass$Collection"/> の末尾に追加します。
   /// </summary>
   /// <param name="value"><see cref="$CollectionTargetClass$Collection"/> の末尾に追加する <see cref="$CollectionTargetClass$"/> 。</param>
   /// <returns>新しい要素が挿入される位置の 0 から始まるインデックス。</returns>
   public int Add($CollectionTargetClass$ value )
   {
      return List.Add(value);
   }

   /// <summary>
   /// 指定した <see cref="$CollectionTargetClass$"/> オブジェクトのコレクション内のインデックスを取得します。
   /// </summary>
   /// <param name="value">検索するオブジェクト。</param>
   /// <returns>見つかった場合は、インデックス番号の最も小さい要素のインデックス番号。それ以外の場合は、 - 1。</returns>
   public int IndexOf($CollectionTargetClass$ value)
   {
      return List.IndexOf(value);
   }

   /// <summary>
   /// <see cref="$CollectionTargetClass$"/> を <see cref="$CollectionTargetClass$Collection"/> の指定した位置に挿入します。
   /// </summary>
   /// <param name="index">value が挿入される位置の 0 から始まるインデックス。</param>
   /// <param name="value">挿入する <see cref="$CollectionTargetClass$"/> 。</param>
   public void Insert(int index, $CollectionTargetClass$ value)
   {
      List.Insert(index, value);
   }
   
   /// <summary>
   /// <see cref="$CollectionTargetClass$Collection"/> 内で最初に見つかった <see cref="$CollectionTargetClass$"/> を 削除します。
   /// </summary>
   /// <param name="value"><see cref="$CollectionTargetClass$Collection"/> から削除する <see cref="$CollectionTargetClass$"/> 。</param>
   public void Remove($CollectionTargetClass$ value)
   {
      List.Remove(value);
   }

   /// <summary>
   /// 指定した <see cref="$CollectionTargetClass$"/> が <see cref="$CollectionTargetClass$Collection"/> 内にあるかどうかを確認します。
   /// </summary>
   /// <param name="value"><see cref="$CollectionTargetClass$Collection"/> 内で検索する <see cref="$CollectionTargetClass$"/>。</param>
   /// <returns>value が <see cref="$CollectionTargetClass$Collection"/> に存在する場合は <c>true</c> 。それ以外の場合は <c>false</c> 。</returns>
   public bool Contains($CollectionTargetClass$ value )
   {
      // If value is not of type $CollectionTargetClass$, this will return false.
      return List.Contains(value);
   }

   #region override
   /// <summary>
   /// 挿入時に呼び出されるもの。一致しないと例外なげます。
   /// </summary>
   /// <param name="index">位置</param>
   /// <param name="value">$CollectionTargetClass$</param>
   protected override void OnInsert(int index, Object value)
   {
      if (value.GetType() != typeof($System$.$CollectionTargetClass$))
         throw new ArgumentException( "value must be of type $System$.$CollectionTargetClass$. (" + value.GetType().ToString() + ")" , "value" );
   }

   /// <summary>
   /// 削除時に呼び出されるもの。
   /// </summary>
   /// <param name="index">位置</param>
   /// <param name="value">$CollectionTargetClass$</param>
   protected override void OnRemove(int index, Object value)
   {
      if (value.GetType() != typeof($System$.$CollectionTargetClass$))
         throw new ArgumentException( "value must be of type $System$.$CollectionTargetClass$. (" + value.GetType().ToString() + ")" , "value" );
   }

   /// <summary>
   /// Set時に呼び出されるもの。
   /// </summary>
   /// <param name="index">位置</param>
   /// <param name="oldValue">古いの</param>
   /// <param name="newValue">新しいの</param>
   protected override void OnSet(int index, Object oldValue, Object newValue)
   {
       if ( newValue.GetType() != typeof($System$.$CollectionTargetClass$))
         throw new ArgumentException( "newValue must be of type $System$.$CollectionTargetClass$. (" + newValue.GetType().ToString() + ")", "newValue" );
   }

   /// <summary>
   /// OnValidate
   /// </summary>
   /// <param name="value">$CollectionTargetClass$</param>
   protected override void OnValidate(Object value)
   {
      if (value.GetType() != typeof($System$.$CollectionTargetClass$))
         throw new ArgumentException( "value must be of type $System$.$CollectionTargetClass$. (" + value.GetType().ToString() + ")" );
   }
   #endregion
}
$end$
            ]]>
         </Code>
      </Snippet>
   </CodeSnippet>
</CodeSnippets>

 なんと、XMLサマリーもスニペットには入るのだぞ。
 むしろ、サマリーをスニペットに登録しておきたいね!override関係のメソッドのサマリーとか、いちいち面倒なんだよ!


2006.10.10

コードスニペット

手動トラックバック先URI
http://www.studio-odyssey.net/content/note/archive01.htm#j1006

 ちょっとしたコードスニペット。
 僕が使いやすいように作ってあるので、みんなが使いやすいかは、知らない。

 コードスニペットの作り方がわからんがなーという人は、これを見ればわかると思うので、それも含めて。

 このコードスニペットは、データベースのコネクションを張って、readerを回して、finallyで閉じるコードです。
 tryブロックは、catchを持ってないので、必要であれば実装。

 2種類あって、datareadermの方は、コネクションが開かれていなければ開いて、閉じるもの。すでに開かれていれば、閉じたりはしない。
 そんなスニペット。

 使うには、My DocumentsフォルダにVisual Studio 2005\Code Snippets\Visual C#\My Code Snippetsと、デフォルトではあるので、そん中に突っ込む。
 あとは、datareaderとやって、tabを2回。

 そんな感じ。

<?xml version="1.0" encoding="utf-8" ?>
   <CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
      <CodeSnippet Format="1.0.0">
         <Header>
            <Title>DataReaderSnippet</Title>
            <Shortcut>datareader</Shortcut>
            <Description>ConnectionをOpenして、リーダを回して、finallyでCloseするまでの一連のスニペット。</Description>
            <Author>studio Odyssey</Author>
            <SnippetTypes>
               <SnippetType>Expansion</SnippetType>
            </SnippetTypes>
         </Header>
         <Snippet>
            <Declarations>
               <Literal>
                  <ID>connection</ID>
                  <ToolTip>利用するConnectionのインスタンス名</ToolTip>
                  <Default>connection</Default>
               </Literal>
               <Literal>
                  <ID>DataReader</ID>
                  <ToolTip>利用するDataReader</ToolTip>
                  <Default>IDataReader</Default>
               </Literal>
               <Literal>
                  <ID>reader</ID>
                  <ToolTip>利用するDataReaderのインスタンス名</ToolTip>
                  <Default>reader</Default>
               </Literal>
               <Literal>
                  <ID>command</ID>
                  <ToolTip>利用するCommandのインスタンス名</ToolTip>
                  <Default>command</Default>
               </Literal>
            </Declarations>
            <Code Language="csharp">
            <![CDATA[
$DataReader$ $reader$ = null;

try
{
   $connection$.Open();

   //Prarameters implement

   $reader$ = $command$.ExecuteReader();

   if ($reader$.HasRows)
   {
      while ($reader$.Read())
      {
         //Reader implement
      }
   }
}
finally
{
   if ($reader$ != null)
   {
      if (!$reader$.IsClosed)
         $reader$.Close();
      $reader$ = null;
   }

   if ($connection$.State != ConnectionState.Closed)
      $connection$.Close();
}
$end$
            ]]>
         </Code>
      </Snippet>
   </CodeSnippet>
</CodeSnippets>
<?xml version="1.0" encoding="utf-8" ?>
   <CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
      <CodeSnippet Format="1.0.0">
         <Header>
            <Title>DataReaderSnippet</Title>
            <Shortcut>datareaderm</Shortcut>
            <Description>ConnectionをOpenして、リーダを回して、finallyでCloseするまでの一連のスニペット。</Description>
            <Author>studio Odyssey</Author>
            <SnippetTypes>
               <SnippetType>Expansion</SnippetType>
            </SnippetTypes>
         </Header>
         <Snippet>
            <Declarations>
               <Literal>
                  <ID>isOpenMine</ID>
                  <ToolTip>ConnectionOpenのための一次変数。</ToolTip>
                  <Default>isOpenMine</Default>
               </Literal>
               <Literal>
                  <ID>connection</ID>
                  <ToolTip>利用するConnectionのインスタンス名</ToolTip>
                  <Default>connection</Default>
               </Literal>
               <Literal>
                  <ID>DataReader</ID>
                  <ToolTip>利用するDataReader</ToolTip>
                  <Default>IDataReader</Default>
               </Literal>
               <Literal>
                  <ID>reader</ID>
                  <ToolTip>利用するDataReaderのインスタンス名</ToolTip>
                  <Default>reader</Default>
               </Literal>
               <Literal>
                  <ID>command</ID>
                  <ToolTip>利用するCommandのインスタンス名</ToolTip>
                  <Default>command</Default>
               </Literal>
            </Declarations>
            <Code Language="csharp">
            <![CDATA[
bool $isOpenMine$ = ($connection$.State != ConnectionState.Open);

$DataReader$ $reader$ = null;

try
{
   if ($isOpenMine$) $connection$.Open();

   //Prarameters implement

   $reader$ = $command$.ExecuteReader();

   if ($reader$.HasRows)
   {
      while ($reader$.Read())
      {
         //Reader implement
      }
   }
}
finally
{
   if ($reader$ != null)
   {
      if (!$reader$.IsClosed)
         $reader$.Close();
      $reader$ = null;
   }

   if ($isOpenMine$ && $connection$.State != ConnectionState.Closed)
      $connection$.Close();
}
$end$
            ]]>
         </Code>
      </Snippet>
   </CodeSnippet>
</CodeSnippets>

 需要のあるなしはともかく、防備録。


2006.10.06

すっげ、忙しい

手動トラックバック先URI
http://www.studio-odyssey.net/content/note/archive01.htm#j0606

 めちゃくちゃ忙しい。
 つーか、1週間で1つのシステム立ち上げたよ。0から書いたよ。VisualStudio2005だよ。生産性高いなー、2005。使いこなせればだけど。
 34個くらいのクラス群しかないから、たいした事はないのかもしれないけれど。

 それはともかく、このシステムの別の部分で、Webからのエントリ部分があったのだけれど、使うのが自分たちなので、テクトーでいいよと言っていた部分を作ってもらっていたのだけれど、GridView(DataGridView)と、DetailViewで適当にやっちゃえばいいだろって思っていたら、この手のドキュメントが、ないのね。なんだよ。これでやれば、すげー楽になるんじゃないのかよ!

 と、いうことで、防備録をかねて、書いておく。
 長いよ!すごく長いよ!
 なにしろ、完全に出来るまでを書くからね!

 キーワードは、ASP.net、.net2.0、GridView、DetailView、DataGridView、型付きDataSet、連動、っていうところか。

 ほんじゃまー、今回は、MVCで言うところの、MVだけを作ります。Cを作るのはそれほど難しくはないです。ちょっと頭を使えば。属性でマークするだけだしね。.net 2.0はリフレクション使いまくりだな。

 データベースサーバに、SQL Server2005を使います。業務的には、そうだろ。
 以下、Tableの生成のためのスクリプト。

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[CodeType]') AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[CodeType](
[CodeType] [int] NOT NULL,
[CodeTypeName] [nvarchar](40) NOT NULL,
 CONSTRAINT [PK_CodeType] PRIMARY KEY CLUSTERED 
(
[CodeType] ASC
)WITH (PAD_INDEX  = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
END
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[CodeTypeList]') AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[CodeTypeList](
[CodeType] [int] NOT NULL,
[CodeNo] [int] NOT NULL,
[SortKey] [int] NOT NULL,
[CodeNoName] [nvarchar](40) NOT NULL,
 CONSTRAINT [PK_CodeTypeList] PRIMARY KEY CLUSTERED 
(
[CodeType] ASC,
[CodeNo] ASC,
)WITH (PAD_INDEX  = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
END
GO
IF NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[dbo].[FK_CodeTypeList_CodeType]') AND parent_object_id = OBJECT_ID(N'[dbo].[CodeTypeList]'))
ALTER TABLE [dbo].[CodeTypeList]  WITH CHECK ADD  CONSTRAINT [FK_CodeTypeList_CodeType] FOREIGN KEY([CodeType])
REFERENCES [dbo].[CodeType] ([CodeType])
GO
ALTER TABLE [dbo].[CodeTypeList] CHECK CONSTRAINT [FK_CodeTypeList_CodeType]

INSERT INTO [dbo].[CodeType] ([CodeType], [CodeTypeName]) VALUES (1, 'TestType');

 ついで、DataSetを作ります。どう作ってもいいです。
 まぁ、プロジェクト分けておいた方がいいでしょう。
 とりあえず、CodeTypesDataSetと名付けて起きます。

 この時、生成されるUpdate文とDelete文が、自分が欲しているものではないことがあります。つーか、たぶん、違います。
 DetailViewは、編集して、削除したり編集したりする時に、リフレクションでこのメソッドを呼ぶのですが、変更可能なデータを見て、更新に行きます。なんで、Originalが存在していない事があります。まぁ、エラーを見て、それを手で追加していくのでもいいんですけどね。

 とりあえず、今回は、CodeTypeListというテーブルから、あるCodeTypeのリストを出して、そのリストを一覧表示に。選択された1行を詳細表示にして、更新させたいと思います。

 なので、そのような結果を返すGetDataを追加します。
 追加の方法は、TableAdapterを右クリックして、クエリの追加です。とりあえず、GetDataByCodeType(FillByCodeType)、GetDataByCodeTypeCodeNo(FillByCodeTypeCodeNo)とゆー、見てわかるとおりのメソッドを作ります。要するに、パラメーターでもらったそれで絞るものです。後者は1行だけを変えすので、Detailに。前者はリストなので、一覧に使えるわけです。

 UpdateメソッドとDeleteを修正して、追加して起きます。どうせ間違いなので。
 Deleteメソッドは、キーのみのものにして起きます。それでいいだろーって。

 そんなわけで、以下のように修正しました。

[Update]
UPDATE         CodeTypeList
SET                CodeType = @CodeType, CodeNo = @CodeNo, SortKey = @SortKey, 
                      CodeNoName = @CodeNoName
WHERE           (CodeType = @Original_CodeType) AND (CodeNo = @Original_CodeNo)
[Delete]
DELETE FROM CodeTypeList
WHERE           (CodeType = @Original_CodeType) AND (CodeNo = @Original_CodeNo)

 で、結局、このUpdateメソッドはこのままだと使えないので、(CodeTypeが常にOriginalなので)partialクラスを作ります。
 具体的には、Updateメソッドのオーバーロードです。ちくちくと、以下のように。

public partial class CodeTypeListTableAdapter
{
        [System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Update)]
        public int Update(int SortKey, string CodeNoName, int original_CodeType, int original_CodeNo)
        {
            return this.Update(original_CodeType, original_CodeNo, SortKey, CodeNoName, original_CodeType, original_CodeNo);
        }
}

 マークされている属性の [System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Update)]があることによって、このメソッドが、DataSourceで一覧にでます。たぶん、未確認ですけど、[System.ComponentModel.DataObjectAttribute(true)]をクラスにつけると、そのクラスがDataSourceで一覧にでると思います。
 リフレクションしすぎー。

 ここまで来たら、次はWebページを作ります。
 まぁ、テクトーにページを作ります。

 作り方の順序は人それぞれなので、適当に作ってください。僕はデータを先にそろえる派なので、データからやります。
 まずは、Listを表示するデータソースを作ります。ツールから、ObjectDataSourceをドラッグドロップします。
 先に名前IDをつけた方がいいようです。あとで変更すると、とぎれる事があるようなので。まぁ、ここでは、名前をCodeTypeListViewSourceとします。

 この子の、データソースを定義します。
 右上の矢印から、データソースを定義を選び、先ほど作ったTableAdapterの、CodeTypeListのGetDataByCodeTypeを選びます。
 UpdateとDeleteとInsertはいらないので、なしにして起きます。

 ちくちく進んでいくと、Select文がパラメータを必要としているので、それをどうするのーと聞かれます。
 今回は面倒なので、QueryStringにしておきます。サイトのページを呼び出す時に、ちゃんと設定してね。(デバッグの時は、サイトのプロパティの開始時の設定でできるので)

 まぁ、適当なQueryString名をつけて、OKです。

 これを利用する、GridViewを作ります。てぺりとポトペタして、データソースを定義します。CodeTypeListViewSourceね。前に作った奴。
 するとGridViewが構成されるので、ここで、選択を有効にしておきます。(つーか、書いてないけど右上の矢印で全部やれる)

 これで一覧は終わり。
 ついで、詳細のDetail。DetailView用のデータソースをぽとぺた。ObjectDataSourceね。名前は、CodeTypeListDetailSourceとしておきます。

 データソースの定義で、1行だけを返すSelect文のやつ、GetDataByCodeTypeCodeNoを選びます。Update文は、先に書いた方のUpdate文にします。2つあると思うんだけどね。
 はてさて、ここで次に進むとわからなくなるのが、Selectのパラメータです。

 やりたいことは簡単で、一覧の方で選択された行のデータがパラメータです。
 なので、そのように設定します。

 具体的には、パラメータの定義画面で、有無を言わせず、詳細表示にします。
 詳細以外はつかえねーよ、ばーや。

 ここで、パラメータソースをControlにします。何となくわかりましたか?
 そうです。ここで、GridViewが選べます。
 ここで、自分がはっつけたGridViewを選びます。これで、「一覧の方で選択された行のデータがパラメータ」のうち、「一覧の方で」まではできました。

 次は、「選択された行」なので、それはSelectedRowか、SelectedDataKeyです。今回はSelectedDataKeyでいいので、SelectedDataKeyにします。

 と、ここでハテナとなるのは、SelectedDataKeyはコレクションであるということです。何を呼べばいいのか。それは、開発者ならわかっているとこです。なので、わかっている通りにします。CodeTypeなら、SelectedDataKey["CodeType"]です。問答無用です。入ります。動きます。
 リフレクションすげぇ。

 ちくちくと設定することによって、目的の「一覧の方で選択された行のデータがパラメータ」はすべて設定できます。

 このDataSourceを利用する、DetailViewを準備します。これもデータソースを定義して、挿入、編集、削除を有効にしておきます。ちくちくと。

 これで、連動は出来ました。
 あとは、コードで、呼び出しのきっかけと、更新についてを書くだけです。
 具体的には、GridViewのDataBound、DetailViewのItemDeleted、ItemInserted、ItemInserting、ItemUpdated、ItemUpdatingを実装します。
 GridViewのDataBoundを実装するのは、Detailに何かをした時、行が0になると、何も選択されていないとよろしくない事になるためです。
 Detailの〜edは、GridViewを最バインドするためで、〜ingは更新前に何かをする最後の機会です。今回は、HtmlEncodeしています。

 そんな訳で、完全なページの実装コードは以下のようになります。

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;

public partial class _Default : System.Web.UI.Page 
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!this.IsPostBack)
        {
            this.CodeTypeListView.DataBind();
        }
    }

    protected void CodeTypeListView_DataBound(object sender, EventArgs e)
    {
        //ばうんどははじめてのロードの時にもよばれるで
        if (this.CodeTypeListView.Rows.Count == 0)
        {
            //なんもないので、新規モードにする
            this.CodeTypeListDetail.ChangeMode(DetailsViewMode.Insert);
        }
        else
        {
            //1行目を選択しておく
            this.CodeTypeListView.SelectedIndex = 0;
        }
    }

    protected void CodeTypeListDetail_ItemDeleted(object sender, DetailsViewDeletedEventArgs e)
    {
        this.CodeTypeListView.DataBind();
    }

    protected void CodeTypeListDetail_ItemInserted(object sender, DetailsViewInsertedEventArgs e)
    {
        this.CodeTypeListView.DataBind();
    }

    protected void CodeTypeListDetail_ItemUpdated(object sender, DetailsViewUpdatedEventArgs e)
    {
        this.CodeTypeListView.DataBind();
    }

    protected void CodeTypeListDetail_ItemInserting(object sender, DetailsViewInsertEventArgs e)
    {
        for (int i = 0; i < e.Values.Count; i++)
        {
            if (e.Values[i] != null)
            {
                e.Values[i] = Server.HtmlEncode(e.Values[i].ToString());
            }
        }
    }
    protected void CodeTypeListDetail_ItemUpdating(object sender, DetailsViewUpdateEventArgs e)
    {
        for (int i = 0; i < e.NewValues.Count; i++)
        {
            if (e.NewValues[i] != null)
            {
                e.NewValues[i] = Server.HtmlEncode(e.NewValues[i].ToString());
            }
        }
    }
}

 これで実行してみると動きます。
 パラメータ名が一致しないとリフレクションが言うことがあるかも知れません。その時は、ObjectDataSourceのパラメーター名を変えるか、Orignalの値の接頭辞をプロパティから変更してみるといいです。
 以外とさくさくと、コーディング無しで単票明細形式の入力が出来てしまうのです。

 リフレクションすげぇ。

 あと、画面をほにゃららしたい〜とか、デザインがなんとか〜というのは、テメーでなんとかしてやってください。
 これも、GridViewとかなら、プロパティがかなり細かく設定できるしね。
 入力されるタイプとかも設定できるんだっけ?WindowsFormではできたなぁ。ASP.net2.0はどうなんだろ。出来そうな気がするけどね。


2006.08.06

そんなコードを書く時点で、何かがおかしいんだろうけど

手動トラックバック先URI
http://www.studio-odyssey.net/content/note/archive01.htm#h0906

 enumをflag的に使って、ほにゃららする。
 Cでいうとこの | これだよ、これ。

 まぁ、そんなのがC#のコードで必要になる時点で、設計が間違っている気もするんだけどさ。

 キーワードは、FalgsAttribute、enum、if、XOR、ANDっていうところか。
 まー、別にどうでもいいんだけどさー。

 つーか、例によってこれが正しい書き方なのかは、知らない。

using System;

namespace ConsoleTest2003
{
   [Flags]
   enum TestTypes : short
   {
      Red = 0x0001,
      Green = 0x0002,
      Blue = 0x0004
   }

   /// <summary>
   /// Class1 の概要の説明です。
   /// </summary>
   class Class1
   {
      [STAThread]
      static void Main(string[] args)
      {   
         TestMethod(TestTypes.Red | TestTypes.Blue);
#if DEBUG
         Console.Read();
#endif
      }

      static void TestMethod(TestTypes type)
      {
         Console.WriteLine("Test:{0}-{1}",(short)type, type.ToString());
         
         if (TestTypes.Red == (type & TestTypes.Red))
         {
            Console.WriteLine("Red OK.");
         }
         if (TestTypes.Green == (type & TestTypes.Green))
         {
            Console.WriteLine("Green OK.");
         }
         if (TestTypes.Blue == (type & TestTypes.Blue))
         {
            Console.WriteLine("Blue OK.");
         }
      }
   }
}

 なーんか、かっちょわるい気がするんだよなー。
 なんだろなー。


2006.06.01

TeamFoundationServer

手動トラックバック先URI
http://www.studio-odyssey.net/content/note/archive01.htm#f0106

 VisualStudio2005のTeamSystem以上についてくる、TeamFoundationServerという製品。
 これ、チーム開発を総合的にサポートするもの。

 この中に、ソースコード管理の機能がある。
 目的は以下。

 VisualStudio.net2003(VS2003)、でも、TeamFoundationServerに付属するソースコード管理機能を使いたい。

 VisualSourceSafe入れるの、めどいじゃーん。

 結論から言って。

http://www.microsoft.com/downloads/details.aspx?FamilyID=87e1ffbd-a484-4c3a-8776-d560ab1e6198&DisplayLang=en
Visual Studio 2005 Team Foundation Server MSSCCI Provider

 落として、インストール。
 以上。

 2006年6月1日現在では日本語版はないけれど、別に問題はない。
 もちろん、出てくるメッセージは英語になってしまうけれど、まぁ、なんとなくわかるだろ!たぶん!きっと!

 ちなみに、チームプロジェクトを作ったりなんだりをするには、チームエクスプローラーが必要なので、VS2005は必要です。はい。

 Officeそろえて、TeamFoundationServerをビジネスに利用するのは、開発者がわかるようになれば、かなりいいものになりそうだなぁと思う。
 問題は、開発者と管理者がこれを覚えきれるかどうかだけど…どうやって覚えさせて、そして何より、利用させるかだよなぁ。

 使い勝手はかなりいいんだけど、テストケースみたいなものがないと、どうしても取っつきにくいかなぁ、これ。

 まずはコード管理と、バグや連絡用のメモ、仕様書をとりあえずぶっこめっていう使い方からしようかしら。(しかし、Workgroupの5ユーザまでの制限がきつい。せめて10ユーザにして欲しかった…テストするにも5では微妙だ…)


2006.05.17

ClickOnce

手動トラックバック先URI
http://www.studio-odyssey.net/content/note/archive01.htm#e1706

 ClickOnceでアプリケーションを配布する時、app.configがあると、manifestファイルより優先されて、完全な信頼のアセンブリが起動しない事がある。
 Windows XPで出るので、Windows XPの問題のような気もする。

 とりあえず、マニフェストファイルの設定がおかしい可能性が高いので、一回マニフェストファイルを削除する。
 しかる後、ClickOnceで配布されたアプリケーションをアンインストールする。
 で、すでにDocument and Settingsの、ユーザー名下にあるLocal Settings\Apps\2.0\に何かしらがあれば、削除する。(実際、ClickOnceアプリケーションをもっていたら、対象のだけ消すですよ。当然ですけど)

 これで入れ直してみる。

 果たして、動くか。

 と、いうのも。

 実は、問題は解決したのだけれど、「解決した理由」がよくわからないのだ。

 あとやったことといえば、app.configの名前を、App.configに変えたくらい。
 でも、これ、戻してもちゃんと動いたし、違うのかなぁ。

 何故直ったのかはわからない。
 多分、カスデータの残りと、ファイル名のあたりの何処かだと思うんだけど…でも、ファイル名だとしたら、戻してエラーにならない理由の説明がつかないしなぁ。

 わからない時は、App.configのままにしておこうかしら。
 それでも問題ないのだし。(というよりこっちの方がなんとなくすっきりすると思うのは僕だけだろうか)

 一応、イベントログの中身をはっ付け。
 同じ悩みを抱えている人がいたら、何かの足しになるかもなので。

マニフェストまたはポリシー ファイル "C:\Documents and Settings\..(中略)..\EventWin.exe.Config" 行 2 に構文エラーが発生しました。 マニフェスト ファイルのルート要素はアセンブリでなければなりません。

 または

Generate Activation Context が C:\Documents and Settings\..(中略)..\EventWin.exe.Manifest に失敗しました。 参照エラー メッセージ : この操作を正しく終了しました。

 結局、何が問題だったんだろうか。


2006.05.16

忘れないように

手動トラックバック先URI
http://www.studio-odyssey.net/content/note/archive01.htm#e1606

 忘れないように、メモ。

 と、言っても、かなり長くなると思うし、日誌としてのものよりも、単に自分があとで困らないように、なので。
 あと、C#でWebサービスプログラミングの話題なので、わからない人は回れ右です。
 今日の日誌はそういうもの。

 やりたいこと。

 WSE3.0を利用して、SAMLによる認証機構を使ったWebサービス。

 実際問題、認証や権限を云々するには、x509証明書を使ったサービスにするのが望ましいと思う。しかし、ユーザー名、パスワードという、「あまりセキュアとは言えない」認証システムの方が、ユーザーには取っつきやすいという感は否めない。そのため、SAMLによって、認証を行い、システムによって権限を承認を行う。つまり、「本人性」と、「システムへのアクセス権」を分離したい。というのが、基本。

 SAMLを選択したのは、上記のような理由と共に、x509署名によるシステムへの移行をスムーズにするためでもある。

 現時点、Webブラウザアプリケーションの多くは「その選択肢がベター」だからという理由によって作られているものは少ない。(そして顧客のオーダーから、ベストでもない場合が多い)
 これは配布の容易さや、ユーザーが「言葉の響き」に騙されているというのもある。が、こちらとしても、売れないものを作っても仕方がないので、これらもふまえつつ、Webサービス+ClickOnceで、CSシステムのようなアプリケーションを設計する。

 はじめに、はここまで。
 で、こっから先は、C#の中級者くらい?で、それなりにx509の知識があって、Webサービスプログラミングができる事を前提とする。
 C#中級者がどのレベルかは、知らない。

 開発環境は、Windows XP SP2で、VisualStudio2005。SQL Server2005(Developer Ed)。SQL Serverは外部サーバに。開発時はVisualStudio2005のパーソナルWebサーバを利用する。(開発の容易さと、企業のセキュリティ問題を考慮するため)

 実行環境としては、Windows XP SP2、またはWindows 2000 Proをクライアントに、Windows 2003 ServerのIIS6.0でWebサービスを公開する。SQL Serverは2005。DMZにWebサーバがおかれるだろうから、SQL Serverは裏に隠れることを前提とする。ただし、今回は2003Serverを1台しか用意できなかったので(さすがにそんなに使えるレベルのPCがないので)、WebサーバとDBサーバは同一のものを使っている。

 まずは、前提知識として、以下のURIにあるMSDNのステップアップを学習し、Webサービス+Windowsクライアントアプリケーションを作れること。
http://www.microsoft.com/japan/msdn/thisweek/step7/#vs2005_DA
Visual Studio 2005 による分散アプリケーション開発編 (全 6 回)

 このハンズオンを学習する事によって、分散アプリケーションの基礎が学べると同時に、VisualStudio2005の操作、及び進化したTableAdapterの利用方法が学べる。(DataSetというべきか)
 実務レベルに引き落とすには、アーキテクトに任せるので、そこまでは触れない。ただし、ここに書かれているやり方で、大抵の業務アプリケーション開発におけるコーディング量は、3割程度は減少すると思う。

 このハンズオンでは、SQL Serverは2005のExpressだが、外部サーバでDevでも問題はない。(実際そうなので)
 総合Windows認証と、SQL Server認証におけるセキュリティ問題その他は、ここでは触れないので、実際の運用時には注意すること。

 第2回の、「データ層の開発 - その 2」では、接続文字列がapp.configに書き込まれる事を確認しておくこと。
 このapp.configの接続文字列は、実際にこれを利用する側で再編集できる。
 いきなり触れておくが、以下のように利用するアプリケーション側で設定できる。(Webサービス側の話になるので、これはWeb.configに書かれる事になる)

<connectionStrings>
<remove name="EventDA.Properties.Settings.Event2005ConnectionString" />
<!-- 一応、これで上書きしておこう -->
<add name="EventDA.Properties.Settings.Event2005ConnectionString"
     connectionString="Data Source=DBServer;Initial Catalog=Event2005DB;Integrated Security=True"
     providerName="System.Data.SqlClient" />
</connectionStrings>

 「第 3 回 ビジネス層の開発 - その 2」で、Webサービスを実装している。
 ここでは、ローカルIISを利用しているが、セキュリティポリシーの関係から、ファイルシステムを利用する事にする。注意すべきは、「Step2」でWebサービスプロジェクトを作る時に、「場所」を(ファイルダイアログの下の方)HTTPではなく、ファイルシステムにすること。
 これで、VisualStudioが提供する、ファイルシステム上のパーソナルWebサーバを利用できるようになる。
 ただし、このパーソナルWebサーバは、ポートが動的に変更されるので、これを固定した方がよい。(クライアントからのWebサービスの呼び出し時に面倒な事になるので)

 固定の方法は、ソリューションエクスプローラーで、Webサイトのプロパティを表示する。
 「動的ポートの使用」というプロパティがあるので、これをfalseにする。(デフォルトはtrue)  その後、ポートを選ぶ。この時のポートは、他のプログラムで利用されていないポートである事。

 「第 3 回 ビジネス層の開発 - その 3」で、テストアプリケーション開発時にうまくWSDLがもってこられない時は、Webサイトをリビルドして実行して見ると大抵の場合はうまくいく。

「第 4 回 プレゼンテーション層の開発 - その 1」は飛ばしてもよい。
 今回はWindowsクライアントアプリを作るので、Webプレゼンテーション層はいらない。
 ただし、ここに書いてある事がすらすらと読んで理解できるレベルでないなら、やっておくべき。

 「第 5 回 分散アプリケーションの開発」は特に必要はない。(認証方式が違うので)
 ただし、SQL Server2005上のデータベースに、Windows2003Server上のNetworkServiceロールのメンバをアクセス可能にする方法については確認しておくこと。SQL Server2005はセキュリティが2000に比べて格段にきつくなっているので、許可しない限りは、同一サーバであってもアクセスできないので注意が必要になる。(開発時はWindows認証を利用していて、それがAdministratorであれば繋がるので気にならないが、IISを実行するユーザはあくまで、NetworkServiceのメンバである。これは今後も重要になる)

 以上で、ファイルシステムWebサーバを利用したVisualStudio2005での簡単な開発方法は理解できる。
 (なお、サンプルのコードは全部VBだが、すべてC#で書き直した)

 これで最も簡単なWebサービス+Windowsクライアントアプリケーションができたので、次にWSE3.0と、SAMLを利用したWebサービスに書き換える。
 WSE3.0はともかく、SAMLを自力で実装するのは骨が折れるので、ここでは、SAML Quickstartを利用する。
 2つのランタイム、及びコードは以下。

http://msdn.microsoft.com/webservices/webservices/building/wse/default.aspx
Web Services Enhancements (WSE)

 もの凄くナチュラルに英語なので、注意。
 と、言うより、3.0の日本語はないので。(多分WCFを先に出すと思うので、3.0が翻訳されるかすら怪しい)

 ダウンロードとインストールはおなじみの通り。
 ランタイムも入ってる。(はず)
 サンプルコードがVBとC#とがあるので、場合によっては片方だけにしてもよし。たいしたサイズではないですが。
 サンプルはインストールディレクトリの下、Samplesフォルダにあるので、見ておくとよいかも知れません。ただし、当然全部英語です。
 基本的にはconfigで設定して使うものがほとんどなので、configの設定方法さえ覚えてしまえば問題はないかもしれません。カスタムのSoapフィルタを作るとかいうのであれば、それなりの構造を理解する必要はあります。あとで触れます。

 SAML Quickstartは、以下のアドレスにあります。
http://www.gotdotnet.com/codegallery/codegallery.aspx?id=8da852b9-2c0d-4eb7-a2de-77222a4075f6
SAML STS for WSE 3.0 QuickStart

 こちらも英語。
 Downloadにはメンバー登録が必要なようです。昔はなかったんだけどなー。
 ライセンスはLicenseを確認してください。商用の利用も可能ですが、copyrightを含める必要があります。コードの変更も許可されています。

 コードの変更ですが、このライブラリ(必要なのは、WseSamlプロジェクトだけですが)にはバグがあります。
 このまま使用すると、ユーザー名やRoleがSAMLから取得できません。このバグの問題により、以下のコードは期待した動作をしません。

[WebMethod]
public EventDA.SessionEntryDataSet GetEntry(int eventID)
{
    EventDA.SessionEntryDataSet ds = new EventDA.SessionEntryDataSet();
    EventBZ.SessionEntry se = new EventBZ.SessionEntry();

    //プロファイルの取得
    int profileID = 0;
    
    //SAMLの権限はここに入ってる
    // しかもこれ、ライブラリがバグってやがって、直したよ!ちくしょぅめ!
    if (RequestSoapContext.Current.Envelope.Context.IdentityToken.Principal.IsInRole("Administrator"))
    {
         profileID = 18;
    }

    //セッションリスト取得
    ds = se.GetEntry(eventID, profileID);
    return ds;
}

 このバグをfixするには、SamlPolicyAssertion.csファイル内にある、ServiceInputFilterクラスのValidateMessageSecurityメソッドを修正する必要があります。

//The signing token must be one of the configured trusted issuers
foreach (X509SecurityToken trustedToken in _trustedTokens)
{
    if(trustedToken.Equals(samlToken.SigningToken))
    {
        trusted = true;
        break;
    }
}

if (!trusted)
{
    throw new InvalidOperationException(Resources.UntrustedSTS);
}

envelope.Context.OperationState.Set(samlToken);
envelope.Context.IdentityToken = samlToken;

 上記のコードを、以下のように修正します。

//The signing token must be one of the configured trusted issuers
foreach (X509SecurityToken trustedToken in _trustedTokens)
{
    if(trustedToken.Equals(samlToken.SigningToken))
    {
        trusted = true;
        break;
    }
}

if (!trusted)
{
    throw new InvalidOperationException(Resources.UntrustedSTS);
}

envelope.Context.Credentials.UltimateReceiver.SetClientToken<SamlToken>(samlToken);    //Added this line

envelope.Context.OperationState.Set(samlToken);
envelope.Context.IdentityToken = samlToken;

 これで期待する動作になります。
(現在のリリースでは修正されているかも知れませんが、わかりません)

 SAML STS for WSE 3.0 QuickStartにはサンプルコードも含まれているので、動作に関してはその辺りも確認するといいです。
 インストール方法等は、zipファイル内のpdfファイルを確認してください。サンプルを動かすようにするのも一苦労かとは思いますが。

 さて、これでツールは揃いましたので、動作の原理について考えます。
 WSE3.0は、ASP.net Webサービスで、通信部分をラップするようなイメージです。
 開発者はその内部を理解する必要はないように設計されていますし、気にするべきではありません。通常のWebサービスを実装するのと同じように、開発者はメソッドにWebMethodアトリビュートをつけ、そのクラスにMicrosoft.Web.Services3.Policy属性をつけるだけです。

 すでに作成した上記のハンズオンをWSE3.0を利用して、SAMLでポリシーを記述する場合のサービス部分のコードは以下のようになります。

using System;
using System.Web;
using System.Collections;
using System.Web.Services;
using System.Web.Services.Protocols;
using Microsoft.Web.Services3;

/// <summary>
/// SessionEntry の概要の説明です
/// </summary>
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[Microsoft.Web.Services3.Policy("Sign-Saml")]
public class SessionEntry : System.Web.Services.WebService
{
    [WebMethod]
    public EventDA.SessionEntryDataSet GetEntry(int eventID)
    {
        EventDA.SessionEntryDataSet ds = new EventDA.SessionEntryDataSet();
        EventBZ.SessionEntry se = new EventBZ.SessionEntry();

        //プロファイルの取得
        int profileID = 0;
        
        //SAMLの権限はここに入ってる
        // しかもこれ、ライブラリがバグってやがって、直したよ!ちくしょぅめ!
        if (RequestSoapContext.Current.Envelope.Context.IdentityToken.Principal.IsInRole("Administrator"))
        {
            profileID = 18;
        }

        //セッションリスト取得
        ds = se.GetEntry(eventID, profileID);

        return ds;
    }

    [WebMethod]
    public int UpdateEntry(EventDA.SessionEntryDataSet ds)
    {
        EventBZ.SessionEntry se = new EventBZ.SessionEntry();
        return se.UpdateEntry(ds);
    }
}

 僕は開発者に新しい苦労をさせるべきではないと思いますし、するべきではないと思います。
 属性こそ、初めて見るようなものがつけられてはいますが、開発者は多くの新しい知識を必要とせず、WSEを利用した、SAMLによる認証機構をスムーズに作れます。

 それでは、実際の動きを考えます。
 動きは簡単で、クライアントは「SAMLをもらい、Webサービスにアクセスする」という構造です。
 まずは、SAMLを発行するサーバサイドのWebサービスを実装します。
 これはSAML STSのサンプルにあるSamlSecurityTokenServiceプロジェクトをベースにします。

 このWebサービスプロジェクトには、asmxファイルはありません。全ての設定はWeb.configと、samlPolicy.configに記述されます。ちなみに呼び出されるのは、<System.Web>Elementの<httpHandlers>Elementに書かれています。

 このサービスは、ユーザーの認証と権限の取得に関して、ASP.net2.0の新機能である、membershipを利用しています。設定は、%lt;membership%gt;Elementを参照してください。
 メンバーシップAPIの実装、及び動作に関する知識は、下記のURIを参照してください。
http://www.microsoft.com/japan/msdn/thisweek/step7/aspnet/MembershipAndRole/memberandrole1.aspx
ステップ 7 ハンズオン : ASP.NET 2.0 メンバーシップとロールの管理 その 1

 この例ではMDBを利用していますが、SQL Server等でもできます。
 この例の中では、machine.configを出していますが、このファイルを変更するべきできありません。
(もっといい奴がMSDNにあったような気がするんだけど、どこにいってしまったのだろう…)

 今回はSQL Serverを利用しました。
 ユーザー名とパスワード、そして権限(ロール)を利用しています。メンバーシップAPIの部分のconfigだけを抜き出すと、以下のようになります。

    <!--
        メンバーシップAPIの設定です。
        defaultProvider:省略可能なString型の属性で、規定のメンバシッププロバイダの名前です。
    -->
    <membership defaultProvider="SqlProvider" userIsOnlineTimeWindow="15">
      <providers>
        <clear /><!--これで、規定のメンバシップは利用されなくなる-->
        
        <!--
            新しい規定のメンバシップです。
            場合によってはapplicationNameをユーザ毎に切り替える必要があるかもしれないです。
            
            name:プロバイダ名です。
            connectionStringName:connectionStrings要素の1つを設定します。
            applicationName:一意のアプリケーション名を指定します。
            enablePasswordReset:ユーザにパスワードのリセットが許可されているか。
            enablePasswordRetrieval:ユーザにパスワードの呼び出しが許可されているか。
            requiresQuestionAndAnswer:秘密の質問に答えるべきであること。
            requiresUniqueEmail:登録されるe-mailがユニークであること。
            maxInvalidPasswordAttempts:メンバシッププロバイダがパスワードをロックするまでの試行回数。
            passwordAttemptWindow:パスワード間違いを保持する時間の分。この時間内にmaxInvalidPasswordAttempts回間違うと、パスワードはロックされる。
            passwordFormat:パスワードを格納する時の処理。ここでは一方向ハッシュ。SQLサーバでないとダメだけど、salt値でハッシュされる。
        -->
        <add name="SqlProvider"
             connectionStringName="MembershipDatabase"
             applicationName="SAML"
             enablePasswordReset="true"
             enablePasswordRetrieval="false"
             requiresQuestionAndAnswer="false"
             requiresUniqueEmail="false"
             maxInvalidPasswordAttempts="5"
             passwordAttemptWindow="10"
             passwordFormat="Hashed"
             type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
      </providers>
    </membership>
    <roleManager defaultProvider="SqlProvider" enabled="true" cacheRolesInCookie="true" cookieName=".ASPROLES" cookieTimeout="30" cookiePath="/" cookieRequireSSL="false" cookieSlidingExpiration="true" cookieProtection="All">
      <providers>
        <add name="SqlProvider" type="System.Web.Security.SqlRoleProvider" connectionStringName="MembershipDatabase" applicationName="SAML" />
      </providers>
    </roleManager>

 実際の接続文字列は、connectionStringName属性に書かれているものを見ます。これはconfigに別に記述します。
 何故このような仕様になっているのかというと、ハンズオンでのデータベースの接続文字列の部分を確認してください。ASP.net 1.xから比べて、データベースアプリケーションの作成が2.0では容易になっているだけでなく、セキュリティを意識して、この接続文字列を暗号化する事も可能です。(DPAPIで暗号化をするためのものを自力で実装する必要はありません)

 この方法については、以下のURIを参照してください。
http://www.microsoft.com/japan/msdn/enterprise/pag/securityguidance/paght000005.aspx
How To: DPAPI を使用して ASP.NET 2.0 内の構成セクションを暗号化する方法

 実際の実装は、以下のようになります。

  <!--
      接続文字列の定義はここで設定できるようになりました。
      接続文字列の設定です。納品するサーバでは、暗号化するべきです。
      nameが使う時の、一意になる名前です。(上書きされるです)
      addでいくつも定義できるようになっています。
      Integrated Security=SSPI;を使うには、Windows認証で、サーバにこのユーザが繋げないとためです。
      これは暗号化できるです。暗号化については、下をみるです。
  -->
  <connectionStrings>
    <add name="MembershipDatabase" connectionString="Server=DBServer;Uid=aspnetdb;pwd=aspnetdb" />
  </connectionStrings>

  <!--
      暗号化のサポートのためのものです。
      これはユーザストアを利用した、DPAPIによる暗号化です。
      aspnet_regiis -pef "connectionStrings" "F:\OSIGOTO\Test\FormsAuthSQL" -prov "MyUserDataProtectionConfigurationProvider"
      で、connectionStringsのセクションが、ユーザストアで暗号化されるです。最後の引数は、任意のnameにできるです。
      ただ、DPAPIである以上、キーはOSが管理するユーザーに対して1つのものなので、複数並べ立てても意味はないです。
      復号化するには
      aspnet_regiis -pdf "connectionStrings" "X:\OSIGOTO\Test\FormsAuthSQL"
      です。
  -->
  <configProtectedData>
    <providers>
      <add useMachineProtection="false"
           keyEntropy=""
           name="MyUserDataProtectionConfigurationProvider"
           type="System.Configuration.DpapiProtectedConfigurationProvider, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
    </providers>
  </configProtectedData>

 実際のサーバ上では、セキュリティのために暗号化するべきです。

 このSAMLを発行するサービスと、クライアントアプリケーションは、WSEを利用して、セキュリティを考慮します。
 そのため、<System.Web>Elementには、<webService>Elementをつけて、拡張する必要があります。
 以下のコードが、<System.Web>Element内に必要です。

<webServices>
  <soapExtensionImporterTypes>
    <add type="Microsoft.Web.Services3.Description.WseExtensionImporter, Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
  </soapExtensionImporterTypes>
</webServices>

 これらはSAML STSのサンプルコードに記載されているものなので、サンプルコードをまずは動かせるようにするべきです。
 そして、必要に応じて、拡張していくと理解できます。

 なお、実際の納品サーバにおけるメンバーシップの追加等をIIS上から行う方法等については、以下のURIにあるサンプルを参照するとよいと思います。(僕はまだざっとしか確認してないですが)
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnaspp/html/ASP2memroleman.asp
Microsoft ASP.NET 2.0 Member/Role Management with IIS, Part 2: Implementation

 英語ですが。

 さて、実際の細かい設定は、<configSections>で追加される新たなElementによります。ここでは、<microsoft.web.services3>と<WseSaml>です。
 コメントをつけた、完全なconfigは以下です。

  <!--
      WSE3.0用の設定
      ここでWSE3.0を設定する。この設定は多岐にわたるのと、複雑なので、ちょっと覚悟するように。
  -->
  <microsoft.web.services3>
    <!--
        デバッグとかのためのトレース情報を格納するか。また、それのファイル名。
        デバッグのために行き来するデータを表示したり、内部のログを収集するには、trueにする。
    -->
    <diagnostics>
      <trace enabled="true" input="samlAsserionServer_InputTrace.webinfo" output="samlAsserionServer_OutputTrace.webinfo" />
    </diagnostics>

    <!--
        トークン(ユーザー認証データ)の発行に対する設定。
        statefulSecurityContextTokenというのは、セキュリティに関するトークンを保持するかどうか
    -->
    <tokenIssuer>
      <statefulSecurityContextToken enabled="true" />
    </tokenIssuer>

    <!--
        セキュリティに関する設定はいろいろと細かいです。
        keyIdentifierMappingは定義です。
        securityTokenManagerはトークンを解析するためのクラスです。
    -->
    <security>
      <keyIdentifierMapping>
        <add valueType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID"
             tokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1" />
      </keyIdentifierMapping>

      <securityTokenManager>
        <!-- AssertionにSAMLを使用する。その使用はCustomSamlTokenManagerを用いる。 -->
        <add localName="Assertion" namespace="urn:oasis:names:tc:SAML:1.0:assertion"
             type="BCL.Web.Services.WSE3.SAML.CustomSamlTokenManagers.CustomSamlTokenManager, BCL.Web.Services.WSE3.SAML.CustomSamlTokenManagers"/>
        <!-- UserNameTokenを使用するクラスです。実際はこの中で処理されて、いろいろいじくられます。 -->
        <add localName="UsernameToken" namespace="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
             type="BCL.Web.Services.WSE3.SAML.CustomSamlTokenManagers.CustomUsernameTokenManager, BCL.Web.Services.WSE3.SAML.CustomSamlTokenManagers" />
      </securityTokenManager>

      <!-- キーアルゴリズムのハッシュに利用されるもの。 -->
      <binarySecurityTokenManager>
        <add valueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3">
          <keyAlgorithm name="RSA15" />
        </add>
      </binarySecurityTokenManager>

      <!--
          x509署名に関する設定。
          <x509 allowTestRoot="true" verifyTrust="false" />
          上記はテスト用。最も強力にするには、下記。
          <x509 allowTestRoot="false" revocationMode="Online" verifyTrust="true" />
          これは、Test署名のルートを許可しない。Onlineで署名確認を行う。署名が正しくなければならない。というきつい条件。
          納品サーバではそうするべきかもしれないが、署名の配布問題によっては、一考すべきもの。
      -->
      <x509 allowTestRoot="true" verifyTrust="false" />

      <!--
          xmlの署名には時間が含まれるのだけれど、その時間とサーバが確認する時間との誤差をどれだけ認めるか。
          単位は秒。最低0。即時切れを起こす(と思う)から、86400(24時間)まで。
          ただしこれはセキュリティに関する問題なので、あまり長い時間にするべきではない。
          とりあえず5分くらいが無難。
      -->
      <timeToleranceInSeconds value="300" />
    </security>

    <!-- SAMLに関してのポリシーは別ファイル -->
    <policy fileName="samlPolicy.config" />
    
  </microsoft.web.services3>
  <!--
      SAML拡張を使うために宣言されたものを受け取る先
  -->
  <WseSaml>
    <!--
        トークン発行者としての設定
        ttlInSecondsはtimeToleranceInSecondsと同じ
    -->
    <samlTokenIssuer allowCachingToken="true" ttlInSeconds="300">
      <!--
          このコンフィグはSAMLのトークン発行の署名に関してです。
      -->
      <serviceTokens>
        <!-- SAML Authority certificate -->
        <add uri="http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue/SAML" storeLocation="LocalMachine" storeName="My" findValue="CN=SAML Authority" findType="FindBySubjectDistinguishedName" />
        <!-- Sample service -->
        <add uri="http://localhost:8080/SampleService/Service.asmx" storeLocation="LocalMachine" storeName="My" findValue="CN=SampleService" findType="FindBySubjectDistinguishedName" />
        <add uri="http://localhost:8080/EventSV/SessionEntry.asmx" storeLocation="LocalMachine" storeName="My" findValue="CN=SampleService" findType="FindBySubjectDistinguishedName" />
      </serviceTokens>
      
      <policy name="issuerPolicy" />
    </samlTokenIssuer>
  </WseSaml>

 <securityTokenManager>では、独自の実装をしています。
 これは、SAML STSのサンプルで言うところの、CostomTokenManagerを拡張したものと考えてください。サンプルではここで初めて、SQL Serverを使ったメンバーシップを利用しています。このCostomされたTokenManagerは、それをさらに拡張したものです。
 実際、業務レベルで利用するには、独自に拡張したAssertionや、UsernameTokenを必要とするはずです。

 <WseSaml>Element内の、<samlTokenIssuer>Element(SAMLの表明発行者)では、このSAMLが通用する先と、そのx509署名を設定します。
 このx509署名は、このサービスが実装させるサーバ側に秘密キーをもった証明書である必要があります。サンプルでは1台のPC上でサーバとクライアントが同居するので切り分けがわかりませんが、これらは、サーバサイドが秘密キーを持つ証明書です。(ちゃんと確認はしてないのですけど)
 クライアントサイドには、これらの公開キーを含んだ証明書が必要です。

 証明書に関してよりも先に、samlPolicy.configを見ます。

<policies xmlns="http://schemas.microsoft.com/wse/2005/06/policy">
  <policy name="issuerPolicy">
    <usernameForCertificateSecurity establishSecurityContext="false" renewExpiredSecurityContext="false" requireSignatureConfirmation="false" messageProtectionOrder="SignBeforeEncryptAndEncryptSignature" requireDerivedKeys="true">
      <!--
          サービスをしてよい対象
      -->
      <serviceToken>
        <x509 storeLocation="LocalMachine" storeName="My"
              findValue="E=webmaster@studio-odyssey.net, CN=studio Odyssey CodeCr, OU=PLANET, O=studio Odyssey, L=side, S=7"
              findType="FindBySubjectDistinguishedName" />
      </serviceToken>
      
      <!-- データ保護の設定 -->
      <protection>
        <request signatureOptions="IncludeSoapBody" encryptBody="true" />
        <response signatureOptions="IncludeSoapBody" encryptBody="true" />
        <fault signatureOptions="IncludeSoapBody" encryptBody="false" />
      </protection>
    </usernameForCertificateSecurity>
  </policy>
</policies>

 <serviceToken>で、見慣れないx509証明書があります。すでにこのsubjectから想像可能なように、この証明書は自宅で作ったものです。
 x509証明書は自分がルートになりさえすれば、自宅からも発行できます。
 そのルートを誰が信頼するか、という問題こそありますが、企業の「アプリケーションを保守する」または「サービスの利用を保証する」というレベルで証明書を発行するのはよいのではないかと思います。(お客さんに信頼してもらうというだけの話です。それはアプリケーションだけの問題ではないでしょう)

 証明書サーバは、Windows2000Server、またはWindows2003Serverであれば、自分で作る事が可能です。
 Microsoft証明書サーバで検索すれば、ある程度の情報は揃うでしょう。まずはIISのSSL通信ができるように設定してみてください。

  • Microsoft証明書サーバは、Windowsアプリケーションの追加と削除で追加できます。
  • 証明書サーバをインストールすると、管理ツールで出ます。
  • 証明書サーバは、Windows2003Serverの場合、セキュリティ的に動かない事があります。
  • 可能であれば、IISの設定を見直してください。
  • 証明書サーバがインストールされた後にIISにマッピングする事も可能です。ディレクトリはcertsrv。(規定ではSystem32下)
  • ブラウザから発行要求をした場合は、証明書サービス側で発行をします。(規定の動作)
  • クライアントとSSL通信をするためには、ルート証明書を貰う必要があります。(CertSrvにアクセスするなりなんなりで)

 実際に格納される証明書を確認するには、IE等のブラウザから、[ツール]-[インターネットオプション]-[コンテンツ]-[証明書]で確認できますが、より詳細に複雑な事をする必要が出てきます。
 ここでは基本的に、Microsoft Management Consoleを利用します。
 [ファイル名を指定して実行](Windowsキー+R)で、mmcとタイプして、OKします。すると、コンソールが起動します。[ファイル]-[スナップインの追加と削除]で、表示されるダイアログの中から、証明書を選択します。
 すると、証明書ストアの何処を選ぶかを選択する必要ができます。証明書ストアには、ユーザとマシンがあります。言葉から想像できるように、ユーザ証明書ストアは、実行ユーザがアクセスできるもので、マシンストアはマシン内の各ユーザがアクセスすることがあるものです。

 例えば、すでに想像できるように、サーバサイドでIISが証明書にアクセスするためには、マシンストアにあり、かつ、IISの実行ユーザであるNetworkService(2000Serverでは、ASPNETユーザ)がその証明書にアクセスできなければなりません。

 少なくとも、以下の条件が、サービスサーバとクライアント側で満たされている必要があります。

  • Webサービス側の証明書には秘密キーが含まれていて、IISの実行アカウントが秘密キーにアクセスできなければならない。
  • LocationはLOCAL_MACNIE(ローカルコンピュータ)で、ストアはMY(個人)でなければならない。
  • クライアントはサーバが持つ証明書の公開キーを持つ証明書をもっていなければならない。
  • クライアントが必要とする証明書のうち、クライアントのsamlPolicy.configで設定する証明書以外のものはLOCAL_MACHINEのMYストアになければならない。

 1台のPC上で実行するのではない場合は(大抵そうでしょうが)、このようになっていなければなりません。(ただし、3つめの条件は例外もあります)

 証明書の発行、及びインポート、アクセスの許可その他に関しては、winhttpcertcfgというツールを利用します。
 以下のURIを参照してください。
http://support.microsoft.com/default.aspx?scid=kb%3Bja%3B901183
ASP.NET Web アプリケーションで認証用のクライアント証明書を使用して Web サービスを呼び出す方法

 最低限覚えなければいけない構文は以下です。

  • WinHttpCertCfg.exe -g -c LOCAL_MACHINE\MY -s "IssuedToName " -a "NetworkService "
  • Winhttpcertcfg.exe -i PFXFile -c LOCAL_MACHINE\My -a "AccountName"

 なお、このツールは規定では、Program Files\Windows Resource Kits\Toolsにインストールされます。

 実際に証明書ストアにアクセスできるかどうかは、以下のコードでチェックできます。

using System;
using System.Collections.Generic;
using System.Text;
using System.Security.Cryptography.X509Certificates;

namespace StoreConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            //Create new X509 store called teststore from the local certificate store.
            X509Store store = new X509Store("My", StoreLocation.LocalMachine);

            store.Open(OpenFlags.ReadOnly);

            X509Certificate2Collection storecollection = (X509Certificate2Collection)store.Certificates;

            Console.WriteLine("Store name: {0}", store.Name);

            foreach (X509Certificate2 x509 in storecollection)
            {
                Console.WriteLine("certificate name: {0}", x509.Subject);
                Console.WriteLine("            HasPrivateKey: {0}", x509.HasPrivateKey ? "true" : "false");
                try
                {
                    if (x509.HasPrivateKey)
                        Console.WriteLine("            HasPrivateKey-Size: {0}", x509.PrivateKey.KeyExchangeAlgorithm);
                }
                catch (Exception)
                {
                    Console.WriteLine("            HasPrivateKey-Size: Error");
                }
            }

            //Close the store.
            store.Close();

#if DEBUG
            Console.Read();
#endif
        }
    }
}

 Webサイトで確認する場合には、上記のコードをPage_Loadで実行し、ページに書き込めばいいです。(divでも作って、innerHTMLにでも)

 以上の事を知っていれば、あとはSampleを拡張して行くことによって、WSE3.0と、SAML STSを利用したWebサービスを実装できるようになります。

 なお、カスタムされたSoapFilterを実装するには、WseSamlプロジェクトの、SamlPolicyAssertionクラスを継承して拡張すればよいはずだが、これについては別の機会にする。

 ご意見、ご質問、アドバイス(特に熱望)に関しては、みんなのケイジバンによろしくお願いします。
 これが正しいのかは、わからないしぃ。


2005.12.24

イブも仕事ですよ

手動トラックバック先URI
http://www.studio-odyssey.net/content/note/archive01.htm#l2405

 この情報はAPS.net 1.1の話なので、ASP.net 2.0では利用すべきではありません。
 ASP.net 2.0では、相対パスはある程度、フレームワークが処理してくれます。そして多くの場合、HostingEnvironment クラスと、Uriクラスの新しいメソッドで、同等の事を実現可能です。

 イブも仕事ですよ。

 こーゆーことをやりたかった。

 ASP.netで、ApplicationPathの上が知りたい。

 ほんだけ。

 単に、ApplicationPathというか、ApplicationRootというか、そこからの位置関係をうりうりしたかっただけ。
 ルート直下に./jsというフォルダがあって、その中身を他のページから知りたいんだけど、階層が深くなってくるとわかんねーし、なんとかしてーなーと。

 とりあえず、そんなわけで、やりたいことOK。
 フォルダの構成が、Rootがあって、直下にjsというフォルダとPageというフォルダがあって、その中から相対Uriが作れればいいわけだ。
 そんなん、答え的には../js/なんだけど、ようは../か../../か./かなんて僕の知ったことではないので、手がかりさえあれば、それが自動で生成されるようにしたいわけだ。

 手がかりは2つ。
 jsというフォルダが、実際にルートから見て、どのような相対位置にあるか。
 それと、実際に相対Uriを作って欲しいページのUri。

 ASP.netで書くので、とりあえず、そこにある機能以外は使わない事にする。
 というわけで作った。

//要求されたUriは HttpContext.Current.Request.Url に入ってる
//ここから、HttpContext.Current.Request.Url.AbsoluteUri とすると、生のUriがとれる
//ただ、これだとRequestQueryが入っていたりで使いにくいので
string pureUri = HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path);
string pureAbsUri = HttpContext.Current.Request.Url.AbsolutePath;

//この2つのUriをぐりぐりすると
Uri baseUri = new Uri(pureUri.Substring(0, pureUri.Length - pureAbsUri.Length) + HttpContext.Current.Request.ApplicationPath + "/");

//多分、これは望む結果です
Console.Write(baseUri.AbsoluteUri);

//これで、相対パスからいろいろできるんでー
Uri jsUri = new Uri(baseUri, "./js/common.js");

//ページは実際は、Transferとかも考えるなら、ここから作った方がいい
Uri pageUri = new Uri(baseUri, HttpContext.Current.Request.CurrentExecutionFilePath);

//これがpageUriからのjsUriへの相対パスです
Console.Write(pageUri.MakeRelative(jsUri));

 多分こんな感じでできるはず。


2005.11.13

非同期ででかい処理をするASP.net

手動トラックバック先URI
http://www.studio-odyssey.net/content/note/archive01.htm#k1305

 この情報はAPS.net 1.1の話なので、ASP.net 2.0では利用すべきではありません。
 ASP.net 2.0では、Pageクラスの非同期処理を行うためのメソッド、RegisterAsyncTask か、コールバック用の実装を可能とする、 Pageクラスの ClientScript.GetCallbackEventReference を利用すべきです。

 結論から言って、プログラマなんてやんない方がいい。
 5年くらいプログラマやってきての結論。

 こうしたいとか、こう作りたいとかいろいろと考える事はあるけれど、それが実現される事なんてない。「こういうコトできる?」「出来ますよ」というのはよくある台詞。ただし、続く。「時間と金があれば」

 まぁ、大抵はない。

 結局作られるアプリケーションなんて、汎用性はないし、その場しのぎだし、OOPを取り入れようが、AOPを入れようが、結局そのコードに触る人間やその開発チーム全体の技術力やなんかが関係して、成功なんてしない。あとは会社の政治。
 その中で、出来る限りよいものを作ろうと思うのは、プログラマ。
 多分そう思わない、思えない人は淘汰されていく。結局、すごくよくできる人が必ずしも生き残るとは限らない。そんな世界。

 結論から言って、プログラマなんかやんない方がいい。
 Microsoftだって、どっかの中小企業だって、中身は一緒。多分。それが証拠に、Vistaは再来年に延期っぽい。

 それはともかく。
 まぁ、作りたいものややりたいことはたくさんあるけれど、政治やなんやかんやで出来ないし、開発者のスキルアップなんかも考えながら、N層のアプリがどうだこうだとか開発手法がどうだと言ったところで、できねーもんはできねー。
 目先の1000万と将来的な3000万なら、企業は1000万取りに行く。
 半期で3000取れるなら、それだけど、往々にしてそれはデスマーチと言う。

 そんなこんなで、ASP.netで、ながーい処理をするという話だ。
 ASP.netでクライアントが30秒以上黙り込む処理などをすること自体が間違いのような気もしないでもないのだが、まぁ、やるわけですよ。月次処理とかな。それはASPである必要があるのかと問いたい。激しく問いたい。つか、僕は今までASPで書いたプログラムの90%以上は提案段階でミスってる気がする。それはASPでやる必要などないのではないか?
 まぁ、その辺りは政治。
 苦労するのはプログラマ。
 金を寄越せ。

 30秒もクライアントが黙ってしまっては、怒られるだけなので、「処理中だよー」とか、適当にメッセージを出すことにする。要するに非同期の処理。
 タイマーで監視するプログラムを書いて、DBとか確認して処理して、終わったらおわったよーんと通知して、ページはリロードさせ続けてチェックみたいなコードをついこの間まで書いていたけれど、ASP.net(というかC#)で非同期の処理って、思ったより簡単なのね。
 なので、ASP.netのみで非同期に処理するように書いてみた。つか、ちょっと調べればこの程度のコード、さくっと出てくんじゃんか。
 調べろ、俺。

 既存の技術で応用可能な事象に出会ったとき、多くのプログラマの思考はそこで停止し、新しい技術を利用しようとはしない。

 今日の格言。
 今、適当に思いついただけ。

using System;

namespace WebSite
{
   /// <summary>
   /// AsycExecuteClass は非同期で実行される処理のためのクラス
   /// </summary>
   public class AsycExecuteClass
   {
      /// <summary>
      /// 実行されるもの
      /// </summary>
      /// <param name="duration">この間スレッド待つ</param>
      /// <remarks>
      /// あくまでサンプルなので
      /// </remarks>
      /// <returns>なんでもいいんだけど、bool</returns>
      public bool Exec(int duration)
      {
         //ここが実際処理するトコになる
         System.Threading.Thread.Sleep(duration);

         return true;
      }
   }
}

 非同期で実行されるクラスと、そのメソッド。
 まぁ、こんなものは実装される側に任せるので、適当。
 ここは僕が作る所じゃない。

 で、非同期で実行されるわけだけど、「実行終わったかなー?」を聞かないといけないので、実行終わったかなーを聞く部分を作る。
 ユーザにページのボタンを押してもらうとかでもいいんだけど、それだと絶対に「自動で出来ないの?」とか言われる事が目に見えているので、自動化する。まぁ、JavaScript使うだけだけど。

using System;

namespace WebSite
{
   /// <summary>
   /// ReloadScriptWriter はリロード用のスクリプトを書き出す所でげす。
   /// </summary>
   public class ReloadScriptWriter
   {
      private ReloadScriptWriter() {}

      /// <summary>
      /// リロード用のスクリプトを書き出す所
      /// </summary>
      /// <param name="page"></param>
      public static void Write(System.Web.UI.Page page)
      {
         Write(page, 10000);
      }

      /// <summary>
      /// リロード用のスクリプトを書き出す所
      /// </summary>
      /// <param name="page"></param>
      /// <param name="millisecond">リロードさせるまでの間の秒数。初期値は10秒。</param>
      public static void Write(System.Web.UI.Page page, int millisecond)
      {
         //リロード処理を行うんだけど、実際はlocationを自分にするという仕組みです
         string scriptBlock = @"
<noscript>JavaScriptがoffになっています。<a href='" + page.Request.Url.AbsoluteUri + @"'>ここ</a>をクリックして、操作完了を確認してください。</noscript>
<script language='javascript'>
<!--
   var timeId = setTimeout('reloadMethod()', " + millisecond.ToString() + @");
   function reloadMethod()
   {
      clearTimeout(timeId);
      this.location.href = this.location.href;
   }
//-->
</script>
";
         page.RegisterStartupScript("ReloadScriptWriter", scriptBlock);
      }
   }
}

 書き出し先のページをもらって、そこにJavaScriptをぺきぺき書き出すだけ。
 setTimeoutでミリ秒後にリロードのメソッドを読んで、herfを自分に入れ替える。ただそんだけ。たしか、JavaScriptのreload()は昔、うまくいかなかったような気がするので、こうした。reloadでもうまくいけば、それでいいんでね?
 一応、noscript対応。そも、ASP.netのサイトでJavaScriptがoffの事を考える人なんているのかと問いたいところだが。そこまで考えてねーだろ。そも、うごかねーんじゃねーの?
 これは汎用的なので、どっかで使えるかもナーと思わないでもないけど、適当に作ったので信じてはいけない。

 んで、これで、「処理するところ」「リロード(監視)するところ」が出来たので、「処理を開始するとこ」と「終わったよのとこ」を作る。まぁ、これは1つのaspxで実現できるので、それでまとめてやっちゃうことにする。
 面倒なので、aspxのコードは出さないけど、

<asp:Label id="lblMessage" runat="server"></asp:Label>
<asp:Button id="btnStart" runat="server" Text="これをやると、長い処理だよ"></asp:Button>

 フォームの中に、これがあるだけです。
 で、実際のコード。

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;

namespace WebSite
{
   /// <summary>
   /// WebForm1 の概要の説明です。
   /// </summary>
   public class WebForm1 : System.Web.UI.Page
   {
      protected System.Web.UI.WebControls.Label lblMessage;
      protected System.Web.UI.WebControls.Button btnStart;
   
      /// <summary>
      /// これ、AsycExecuteClassのExecを呼ぶためのデリゲート
      /// </summary>
      public delegate bool ExecDelegate(int duration);

      private void Page_Load(object sender, System.EventArgs e)
      {
         if (this.IsExecute)
         {
            this.SetIdle();
         }
         else
         {
            this.lblMessage.Text = "ぼけっとしてるみたい...";
         }
      }

      #region Web フォーム デザイナで生成されたコード
      override protected void OnInit(EventArgs e)
      {
         //
         // CODEGEN: この呼び出しは、ASP.NET Web フォーム デザイナで必要です。
         //
         InitializeComponent();
         base.OnInit(e);
      }
      
      /// <summary>
      /// デザイナ サポートに必要なメソッドです。このメソッドの内容を
      /// コード エディタで変更しないでください。
      /// </summary>
      private void InitializeComponent()
      {
         this.btnStart.Click += new System.EventHandler(this.btnStart_Click);
         this.Load += new System.EventHandler(this.Page_Load);

      }
      #endregion

      /// <summary>
      /// これは開始〜
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void btnStart_Click(object sender, System.EventArgs e)
      {
         //処理するクラスをnewして
         AsycExecuteClass asycExeC = new AsycExecuteClass();

         //デリゲートをnewする
         ExecDelegate dlgt = new ExecDelegate(asycExeC.Exec);

         //コールバックメソッドをnewして
         AsyncCallback cbm = new AsyncCallback(CallbackMethod);

         //非同期処理開始するために、それらを足す
         IAsyncResult ar = dlgt.BeginInvoke(60000, cbm, dlgt);

         this.IsExecute = true;
         this.lblMessage.Text = "処理開始した!";

         //リロードさせんと
         this.SetIdle();
      }

      /// <summary>
      /// 非同期で処理が終わった後、コールバックされて来るメソッド
      /// </summary>
      /// <param name="ar">非同期操作のステータス</param>
      void CallbackMethod(IAsyncResult ar)
      {
         ExecDelegate dlgt = (ExecDelegate)ar.AsyncState;
         bool ret = dlgt.EndInvoke(ar);

         IsExecute = false;

         //ここに何かクライアントに対しての処理をしようとしても無駄なので注意な
      }


      /// <summary>
      /// 状態はSessionで保持するなり、DBで保持するなり...
      /// </summary>
      protected bool IsExecute
      {
         get
         {
            return (this.Session["__IsExecute"] == null) ? false : (bool)this.Session["__IsExecute"];
         }
         set
         {
            this.Session["__IsExecute"] = value;
         }
      }

      private void SetIdle()
      {
         this.lblMessage.Text = "処理してるみたい...";

         this.btnStart.Enabled = false;

         //リロードさせんと
         ReloadScriptWriter.Write(this);
      }
   }
}

 なんかusingがスゲーんだけど使ってないです。
 つか、単に面倒なのでusingが多いだけです。いらん。
 コードは完全なコードを書くのが僕のジャスティスなので完全なコードなのですが、実際、分けて考えるとたいしたことはなくて、まー、Page_Loadから見るのが妥当だろう。

      private void Page_Load(object sender, System.EventArgs e)
      {
         if (this.IsExecute)
         {
            this.SetIdle();
         }
         else
         {
            this.lblMessage.Text = "ぼけっとしてるみたい...";
         }
      }

 JavaScriptがlocationのhrefを自分にhrefするので、常にIsPostBackはfalseなんですよ。
 なので、そのため、ここでPostBackだったらとかいう処理の記述をするのはナンセンスなわけです。ここでやってるのは、IsExectueがtureだったら処理中。そうでなければ処理してないということ。IsExecuteの実装がどうなっているかというと、

      /// <summary>
      /// 状態はSessionで保持するなり、DBで保持するなり...
      /// </summary>
      protected bool IsExecute
      {
         get
         {
            return (this.Session["__IsExecute"] == null) ? false : (bool)this.Session["__IsExecute"];
         }
         set
         {
            this.Session["__IsExecute"] = value;
         }
      }

 Sessionに保持している現在の状態を帰すだけ。
 これが状態がいくつもあるならenumでもすればいいし、Sessionだとユーザ単位なのでアプリケーション単位にとかいうのであれば(アプリケーションというのか?)DB使ったりstaticしたりやり方はそれぞれ。まぁ、それは実装次第。DBが一番楽だろうけどなー。

      private void SetIdle()
      {
         this.lblMessage.Text = "処理してるみたい...";

         this.btnStart.Enabled = false;

         //リロードさせんと
         ReloadScriptWriter.Write(this);
      }

 処理中は処理してますよっと。ボタン押せなくしておく。
 あとリロード用のスクリプト書く。

 実際はボタンを押された時に非同期で処理を開始するので、

      /// <summary>
      /// これは開始〜
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void btnStart_Click(object sender, System.EventArgs e)
      {
         //処理するクラスをnewして
         AsycExecuteClass asycExeC = new AsycExecuteClass();

         //デリゲートをnewする
         ExecDelegate dlgt = new ExecDelegate(asycExeC.Exec);

         //コールバックメソッドをnewして
         AsyncCallback cbm = new AsyncCallback(CallbackMethod);

         //非同期処理開始するために、それらを足す
         IAsyncResult ar = dlgt.BeginInvoke(60000, cbm, dlgt);

         this.IsExecute = true;
         this.lblMessage.Text = "処理開始した!";

         //リロードさせんと
         this.SetIdle();
      }

 ExecDelegateってのは、上の方で宣言してる。デリゲートの理解が未だによくわかんないんだけど、関数ポインタ的なものという理解でいいんだろうか。的なものと書いたのは、それとは違うんだけど、使い方は大してかわんないなーという感覚から来てるんだけど、そういうモンなんだろうか…よくわかんない。MSDNに何か書いてあった気がするけれど、まー、いいや。
 宣言したAsycExecuteClassの、Execメソッドのカタチをしたデリゲートをnewしたら、それにAsycExecuteClassのインスタンスasycExeCのExecをパクリと食わせる。と、dlgtはそれが呼べるわけだから、その理解でいいやと思ってる。で、非同期で実行するので、終わったあとに呼び出されるコールバックメソッドを作る。それはCallbackMethod。あとで書くとして、このクラスのメンバ。これを非同期でnewする。
 BeginInvokeで実行されるので、そこで引数と、コールバックメソッドへの参照と、デリゲートへの参照を投げ込む。この辺、よくわかんない。実装上の問題だと思うんだよね、三番目にデリゲートを引数で持つの。まぁ、回収されちゃいそうな気がするしね、ないと。よくわかんないけど、多分そんなモンだろ。(MSDN見てない)

 処理を開始したのでしましたよっとSessionに入れて、リロード用のスクリプトを書く。
 これで処理中の監視部分はおけ。

      /// <summary>
      /// 非同期で処理が終わった後、コールバックされて来るメソッド
      /// </summary>
      /// <param name="ar">非同期操作のステータス</param>
      void CallbackMethod(IAsyncResult ar)
      {
         ExecDelegate dlgt = (ExecDelegate)ar.AsyncState;
         bool ret = dlgt.EndInvoke(ar);

         IsExecute = false;

         //ここに何かクライアントに対しての処理をしようとしても無駄なので注意な
      }

 非同期の処理が終わったらコールバックされてくる。
 処理の中身を引数からもらって取り出して、それのEndInvokeに戻り値が入ってる。ここでは戻り値の判定はどーでもいいんで、書いてないけど、必要なら追加。
 コメントで、//ここに何かクライアントに対しての処理をしようとしても無駄なので注意なと書いてあるんだけど、ここで書いてもクライアントに影響は起こらない。だって、リロードされてくるのはここではないから。でも、本当はわかんない。試してないから。そうじゃねーの?と思って書いてる。動いたらゴメ。要するに、CallbackMethodの実行されるタイミングの問題だよね。それがどこか、そんだけのこと。
 俺はここに何か書いても、意味ないだろうなーと思ってる。
 だからPage_Loadで処理してるわけ。

 これで一応、簡単な非同期処理は出来るはず。
 まぁ、簡単なものなので、マルチスレッドとかは全然考えてないんだけど。

 結局ASPとかJavaで作られるアプリの80%は、それ本来の目指すべきもののためには作られてないと思うんだよね…
 こんなの、Webでやるべき事ではないんだよ…


2005.10.19

app.configを使って、設定をうりうりする

手動トラックバック先URI
http://www.studio-odyssey.net/content/note/archive01.htm#j1905

 この情報はAPS.net 1.1の話なので、ASP.net 2.0では利用すべきではありません。
 ASP.net 2.0では、ConfigurationElement や、ConfigurationSection を利用すべきです。

 クラスライブラリを直していて、まー、汎用性を高めようと思ったわけです。
 で、どうしよっかなーと思いまして、あー、設定出来る項目を一括で設定できるように、外に出しておく方がいいかもしんねーなー。
 とか、思ったわけです。

 log4netとか、app.configに書くじゃん。設定を。
 あんな感じに。

 で、log4netを覗いてみたんですけど、なんかスゲー複雑なんですよ。
 もっと簡単にできないものかなーと思うわけです。はい。

 で、XMLの解釈とかやるトコが.net Frameworkにあるわけで、それを使うかなっと思ったんですけど、これがまた…

 サンプルもドキュメントも、ありゃしねぇ。

 なので、いろいろと調べながらうりうりと書いたのです。
 合ってないかもしんないんだけども、まー、基本はこんなモンだろう。
 細かい所はコードを書き足すとして、作り方です。

 日本語で書いてる所なんて、あんまりないぞー。

 まず、何がしたいのかというとですね。
 app.configに、こういう事をしたいわけです。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>

   <OriginalSettings>
      <WebSite author="studio Odyssey" uri="http://www.studio-odyssey.net/" mail="webmaster@studio-odyssey.net" />
      
      <Contents>
         <Content title="Books Online">
            <Book author="u-1" title="R-0" />
            <Book author="Gravity" title="Ragnarok Online" />
         </Content>
         <Content title="Note Book" />
         <Content title="BBS" />
      </Contents>
   <OriginalSettings>
</configuration>

 あんだすたん?

 おけおけ。

 オリジナルのセクションを作ったので、当然、それを解析しないといけないわけで、それを実装しないといかんのです。
 で、「このノードを解析するのはこれ」みたいな設定を、app.configに追加します。

 だから、最終的にはこういうコードになるのです。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>

   <!--これが追加された-->
   <configSections>
      <!--,の後ろは、アセンブリ名を設定する-->
      <section name="OriginalSettings" type="AppconfigTest.ConfigurationSettings.MyConfigHandler,AppconfigTest" />
   </configSections>
   <!--ここまで-->
   
   <OriginalSettings>
      <WebSite author="studio Odyssey" uri="http://www.studio-odyssey.net/" mail="webmaster@studio-odyssey.net" />
      
      <Contents>
         <Content title="Books Online">
            <Book author="u-1" title="R-0" />
            <Book author="Gravity" title="Ragnarok Online" />
         </Content>
         <Content title="Note Book" />
         <Content title="BBS" />
      </Contents>
   <OriginalSettings>
</configuration>

 んでは、実装に取りかかります。

 オリジナルの解析ロジックはですね、System.Configuration.IConfigurationSectionHandlerを継承したクラスで実装します。
 そのクラスの名前は、ここでは、AppconfigTest.ConfigurationSettings.MyConfigHandlerです。

 なぜなら、そうapp.configに設定したからです。(順序が逆の気もしないでもないけれど)

 そんなわけで、そのようなクラスを作ります。
 それは、IConfigurationSectionHandlerの唯一のインターフェイスである、Createを実装します。

 んで、あとは論より証拠のコードで。
 戻り値に構成されたオブジェクトを返すので、とりあえず、クラスをぺきぺき作っておきます。
 もっと綺麗な実装はあるとおもうんですけどもねー。

using System;

namespace AppconfigTest.ConfigurationSettings
{
   /// <summary>
   /// WebSiteの内容を保持するためのもの
   /// </summary>
   public class WebSite
   {
      public string Author;
      public string Uri;
      public string Mail;
   }

   /// <summary>
   /// Bookの内容を保持するためのもの
   /// </summary>
   public struct Book
   {
      public string Author;
      public string Title;
   }

   /// <summary>
   /// Bookのこれくしょん
   /// 面倒くさいので最低限の実装しかしない
   /// </summary>
   public class BooksCollection : System.Collections.CollectionBase
   {
      public Book this[int index]
      {
         get
         {
            return ((Book)List[index]);
         }
         set
         {
            List[index] = value;
         }
      }

      public int Add(Book value)
      {
         return(List.Add(value));
      }
   }

   public class Content
   {
      public string Title;
      private BooksCollection _books;

      public Content()
      {
         this.Title = "";
         this._books = new BooksCollection();
      }

      public BooksCollection Books
      {
         get
         {
            return this._books;
         }
      }
   }

   /// <summary>
   /// Contectのこれくしょん
   /// </summary>
   public class ContentCollection : System.Collections.CollectionBase
   {
      public Content this[int index]
      {
         get
         {
            return ((Content)List[index]);
         }
      }

      public int Add(Content value)
      {
         return(List.Add(value));
      }
   }

   /// <summary>
   /// 返すべきオブジェクト
   /// 簡単な実装しかしない
   /// </summary>
   public class OriginalSettings
   {
      private WebSite _webSite;
      private ContentCollection _contents;
      
      public OriginalSettings()
      {
         this._webSite = new WebSite();
         this._contents = new ContentCollection();
      }

      public WebSite WebSite
      {
         get
         {
            return this._webSite;
         }
      }

      public ContentCollection Contents
      {
         get
         {
            return this._contents;
         }
      }
   }
   /// <summary>
   /// MyConfigurationSettings は IConfigurationSectionHandler を実装した、解析するためのものです。
   /// </summary>
   public class MyConfigHandler : System.Configuration.IConfigurationSectionHandler
   {
      public MyConfigHandler() {}

      /// <summary>
      /// これが唯一のインターフェイス
      /// </summary>
      /// <param name="parent">対応する親の構成セクションにおける構成設定。 </param>
      /// <param name="configContext">ASP.NET 構成システムから Create を呼び出した場合は HttpConfigurationContext 。それ以外の場合、このパラメータは予約されており、 null 参照 (Visual Basic では Nothing) です。</param>
      /// <param name="section">構成ファイルからの構成情報を格納している XmlNode 。構成セクションの XML の内容に直接アクセスできるようにします。 </param>
      /// <returns>構成オブジェクト。</returns>
      public object Create(object parent,
         object configContext, System.Xml.XmlNode section)
      {
         //返すもの
         OriginalSettings settings = new OriginalSettings();

         System.Xml.XmlNode node;

         node = section.SelectSingleNode("WebSite");

         //nullを返すのかは知らんので、ちゃんと調べた方がいいよ
         if (node != null)
         {
            settings.WebSite.Author = node.Attributes["author"].Value;
            settings.WebSite.Uri = node.Attributes["uri"].Value;
            settings.WebSite.Mail = node.Attributes["mail"].Value;
         }

         node = section.SelectSingleNode("Contents");

         if (node != null)
         {
            //Contentsの中身が取れるわけだ
            foreach (System.Xml.XmlNode chiled in node.ChildNodes)
            {
               Content cont = new Content();

               cont.Title = chiled.Attributes["title"].Value;

               //ここがbook
               foreach (System.Xml.XmlNode chiledchiled in chiled.ChildNodes)
               {
                  Book data;

                  data.Author = chiledchiled.Attributes["author"].Value;
                  data.Title = chiledchiled.Attributes["title"].Value;

                  cont.Books.Add(data);
               }

               settings.Contents.Add(cont);
            }
         }

         return settings;
      }
   }
}

 とまぁ、こんな感じ。
 サンプルコードは全部乗せるのが俺のじゃすてぃす!

 取得の方法は、こんな感じです。

object o = System.Configuration.ConfigurationSettings.GetConfig("OriginalSettings");
AppconfigTest.ConfigurationSettings.OriginalSettings setting = (AppconfigTest.ConfigurationSettings.OriginalSettings )o;

 一応、中身の確認用の完全なコードは以下。

private void button1_Click(object sender, System.EventArgs e)
{
   object o = System.Configuration.ConfigurationSettings.GetConfig("OriginalSettings");
   AppconfigTest.ConfigurationSettings.OriginalSettings setting = (AppconfigTest.ConfigurationSettings.OriginalSettings )o;

   System.Text.StringBuilder sb = new System.Text.StringBuilder();
   sb.Append(setting.WebSite.Author);
   sb.Append(" / ");
   sb.Append(setting.WebSite.Uri);
   sb.Append(" / ");
   sb.Append(setting.WebSite.Mail);

   sb.Append("\r\n");

   foreach (AppconfigTest.ConfigurationSettings.Content cont in setting.Contents)
   {
sb.Append(cont.Title);

foreach (AppconfigTest.ConfigurationSettings.Book bk in cont.Books)
{
   sb.Append("\r\n");
   sb.Append("\t");
   sb.Append(bk.Title + " / " + bk.Author);
}

sb.Append("\r\n");
   }

   textBox1.Text = sb.ToString();
}

 でも、ここまで実装するんなら、自前でXML解析する部分を作った方がいいような気もするなー。


2005.08.19

log4netを使ってみる

手動トラックバック先URI
http://www.studio-odyssey.net/content/note/archive01.htm#h1905

 とりあえず、アーカイブもののテストもかねて、log4netの使い方とか解説とかコードとか。

 log4netというのは、.net用のログためためライブラリ。
 かなり強力。
 使い方難しくないけれど、ドキュメント英語ばかりなので、調べるの面倒くさいしぃ。
 と言う人向けの高速チュートリアル。
 基本的にコード。
 まずは、前提。

  • .net 2003で確認
  • Log4netのバージョンは、1.2.9beta

 まずは、log4netの入手先。
 http://logging.apache.org/log4net/
 リンクが切れていたら、「log4net」とか、「Logging」とか、その辺りで調べればよいかと。Apatch的には、この日誌の日付時点では、Logging Servicesというちょります。

 あぃ、次。
 使い方。

 ダウンロードしたのもを解凍して、どっかに置く。置いた?
 おいたら、ログを吐き出したい.netのプロジェクトの参照設定から、参照する。できたね?(解説もしねーのかよ)
 特に動きだけ確認してーんだよと言う人は、コンソールアプリでもつくりゃんさい。
 以下は、そのコンソールアプリでの使い方で説明するよ。

 作ったら、XmlConfiguratorAttributeを設定しないといかん。どこでするかというと、まぁ、いろいろあるようなんだが、面倒くさいんで、今回はAssemblyInfo.csに設定することにする。
 AssemblyInfo.csを開いたら、以下を書く。

[assembly: log4net.Config.XmlConfigurator(Watch=true)]

 どこでもかまわん。
 log4net.Config.DOMConfiguratorと書いてあるサイトもたくさんだが、一応、古いから使わないでねって怒られるので、使わないことにする。別に動く。
 この設定をすると、読み込まれるログ書き出しの設定を書いたファイルは、アセンブリ名.configとなるそうなんで、そうゆーファイルを.netに書き出して欲しいと思うのが人の常。そんなわけで、プロジェクトを右クリックして、追加の新規で、アプリケーション構成ファイル。ここで、app.configというファイルを作れば、それはビルドするとアセンブリ名.configになるので都合がよい。なお、オマケだが、こちらでファイル名を指定したいばやいは、

[assembly: log4net.Config.XmlConfigurator(ConfigFileExtension="log4net",Watch=true)]

 と、やるそーです。

 次に、config。
 つーか、もうこのあたり、俺的メモ。
 app.configをがーっと掲載。
 コメント入れておく。(忘れるから)

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <!--
   log4netをレジストする。
   まぁ、おまじないなので気にせずかく
    -->
   <configSections>
      <section name="log4net" type="System.Configuration.IgnoreSectionHandler" />
   </configSections>
   
   
   <appSettings>
      <!--
      たぶん、ConfigurationSettings.AppSettingsでデバッグなん?て
      聞くためのもの。
       -->
      <!-- <add key="log4net.Internal.Debug" value="true"/> -->
   </appSettings>
   
   <!--
   こっから、lgo4netの設定をするとこ
   とりあえず、説明。
   
   appenderっつーのが、ようは出力先の設定なのね。まぁ、appenderはおまじない。
   paramで出力ログの設定をする。
   layoutってーのは、出力設定のレイアウト。まんま。
   使えるのは以下。
   
   %c ログ出力が行われたlogger名を出力。
   %C クラスを出力。
   %d 日時を出力。「%d{yyyy/mm/dd HH:mm:ss}」といった詳細指定も可能。
   %F ファイル名を出力。
   %l ソース名や行といった呼び出し位置を出力。
   %L 行番号を出力。
   %m メッセージを出力。
   %M メソッド名を出力。
   %n 改行文字を出力。
   %p ログのレベル(Fatal/Errorなど)を出力。
   %t ログを生成したスレッドを出力。
   %x スレッドのNDC(ネスト化診断コンテキスト) を出力。スレッド固有の情報(セッションIDなど)を埋め込むことができます。
   
   loggerとか、意味がわからんのは、今のトコ、無視していいっす。
   %C、%F、%l、%L、%Mの場合は処理負荷が高くなるそうなんで、必要な時以外は使わない方がいいんだとさ。
   -->
   <log4net>
      <!--
      コンソール出力用の設定
      嫌いだから使わないと思う。
      -->
      <!--<appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
         <layout type="log4net.Layout.PatternLayout">
            <param name="ConversionPattern" value="%d [%t] %-5p %c [%x] - %m%n" />
         </layout>
      </appender>-->
      
      <!--
      ログファイル出力用
      これだけて事足りる事がほとんどだと思う。みれば大抵わかるかと。
       -->
      <appender name="LogFileAppender" type="log4net.Appender.FileAppender">
         <param name="File" value="Sample.log" />
         <param name="AppendToFile" value="true" />
         <layout type="log4net.Layout.PatternLayout">
            <param name="ConversionPattern" value="%d [%t] %-5p %c [%x] - %m%n" />
         </layout>
      </appender>
      
      <!--
      ログファイル出力用
      なんかちょっと難しいもん。
      日付またはファイルのサイズ制約に基づいた多数のログを生成する設定
      -->
      <!--<appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender">
         <param name="File" value="SampleRolling.log" />
         <param name="AppendToFile" value="true" />
         <param name="MaxSizeRollBackups" value="10" />
         <param name="MaximumFileSize" value="100KB" />
         <param name="RollingStyle" value="Size" />
         <param name="StaticLogFileName" value="true" />
         <layout type="log4net.Layout.PatternLayout">
            <param name="ConversionPattern" value="%d [%t] %-5p %c [%x] - %m%n" />
         </layout>
      </appender>
      -->
      
      <!--
      ログファイル出力用
      とっても難しそーなパターン。
      上と違うのは、File、RollingStyle、StaticLogFileName、DatePattern。
      実はこれがミソなんだとさ。
      え?どうなるの?っていうと、ようは日付毎にロギングされるとのこと。
      こういうの考えつく人ってスゲーなー。
      -->
      <!--
      <appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender">
         <param name="File" value="SampleRolling" />
         <param name="AppendToFile" value="true" />
         <param name="MaxSizeRollBackups" value="10" />
         <param name="MaximumFileSize" value="100KB" />
         <param name="RollingStyle" value="date " />
         <param name="StaticLogFileName" value="false" />
         <param name="DatePattern" value='"."yyyy-MM-dd".log"' />
         <layout type="log4net.Layout.PatternLayout">
            <param name="ConversionPattern" value="%d [%t] %-5p %c [%x] - %m%n" />
         </layout>
      </appender>
      -->
      
      <!--
      rootってーのは、基本設定とでも読み替えればよいんでない?
      levelってのが、出力のレベル。レベルは以下の通り。
      
      FATAL   システム停止するような致命的な障害
      ERROR   システム停止はしないが、問題となる障害
      WARN   障害ではない注意警告
      INFO   操作ログなどの情報
      DEBUG   開発用のデバッグメッセージ
      ALL      全部
      
      重要度が自分より高いものは出力されます。
       -->
      <root>
         <level value="DEBUG" />
         <appender-ref ref="LogFileAppender" />
         <!--<appender-ref ref="ConsoleAppender" />--><!--コンソールに書くときはこっちだよ-->
      </root>
      
      <!--
      特定のカテゴリ毎の設定。
      -->
      <!--<logger name="BCL">
         <level value="ALL" />
         <appender-ref ref="RollingLogFileAppender" />
      </logger>-->
      <!--<logger name="BCL.Web">
         <level value="ALL" />
         <appender-ref ref="RollingLogFileAppender" />
      </logger>-->
   </log4net>
</configuration>

 まぁ、こんな感じ。

 テストするならコピペで十分なんじゃなーい?

 では、実際の実装。

class Class1
{
   private static readonly ILog logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

   /// <summary>
   /// アプリケーションのメイン エントリ ポイントです。
   /// </summary>
   [STAThread]
   static void Main(string[] args)
   {
      logger.Debug("Deb");
      logger.Error("Err");
      logger.Info("Info");
      logger.Fatal("Fatal");
   }
}

 あい。
 これで、ログが書き出されます。
 loggerってーのは、ようは、ログマネージャから取得されるもんでち。それに、出力したいものをレベル毎に書き出していくわけですわさ。
 System.Reflection.MethodBase.GetCurrentMethod().DeclaringTypeって何よってなモンですけど、これが、特定のカテゴリ毎の設定ってトコで設定するnameのものです。
 まぁ、使い方次第でいろいろできるんだよっと。

 最後にでっかめ。

#if (!LOG4NET)
#define LOG4NET      //これをコメントすると、Log出さない
#endif

#if (!LOG4NETCONFIG)
//#define LOG4NETCONFIG   //これをコメントすると、configみない
#endif

using System;
using System.IO;
using System.Collections;


namespace ConsoleApplication1
{
#if LOG4NET

   /// <summary>
   /// カテゴリくらい作ってやるか
   /// </summary>
   public enum LogCategory
   {
      Main,
      Action,
      Function,
   }

   /// <summary>
   /// ログの吐き出しクラス
   /// </summary>
   class Logger
   {
      private static SortedList _loggerList = new SortedList();
      private static bool _init = false;

      private Logger() {}

      public static void Init()
      {
         if (_init) return;

#if (!LOG4NETCONFIG)

         //ぶっちゃけ、廃止傾向と文句言われるが、
         //別に動くからちょっとログ取るくらいならいいし
         log4net.Config.BasicConfigurator.Configure(new log4net.Appender.FileAppender(new log4net.Layout.PatternLayout("%d [%t] %-5p %c [%x] - %m%n"), "Logger_pg.log", true));

#else
         FileInfo fi = new FileInfo("Logger.config");

         if (fi.Exists)
         {
            log4net.Config.XmlConfigurator.ConfigureAndWatch(fi);
         }
         else
         {
            //なければ標準出力にだしておくか
            log4net.Config.BasicConfigurator.Configure();
         }

#endif   //LOG4NETCONFIG

         _init = true;

         _loggerList[LogCategory.Main] = log4net.LogManager.GetLogger("Log4netTest.LogCategory.Main");
         _loggerList[LogCategory.Action] = log4net.LogManager.GetLogger("Log4netTest.LogCategory.Action");
         _loggerList[LogCategory.Function] = log4net.LogManager.GetLogger("Log4netTest.LogCategory.Function");
      }

      public static log4net.ILog GetILog(LogCategory category)
      {
         Init();
         return (log4net.ILog)_loggerList[category];
      }

      public static log4net.ILog Main
      {
         get
         {
            return GetILog(LogCategory.Main);
         }
      }

      public static log4net.ILog Action
      {
         get
         {
            return GetILog(LogCategory.Action);
         }
      }

      public static log4net.ILog Function
      {
         get
         {
            return GetILog(LogCategory.Function);
         }
      }
   }
#endif   //LOG4NET


   /// <summary>
   /// これ、本体
   /// </summary>
   class Class1
   {
      /// <summary>
      /// アプリケーションのメイン エントリ ポイントです。
      /// </summary>
      [STAThread]
      static void Main(string[] args)
      {
#if LOG4NET
         Logger.Action.Debug("Act");
         Logger.Function.Debug("Func");
         Logger.Main.Debug("Main");
#endif
      }
   }
}

 実際問題、いちいち、ログを取るのに、それが開発段階でのデバッグ用とかだったら、はっきりいってconfigなんて書くのはウザいんで、このクラスで設定する。
 まぁ、ログをカキカキするに当たって、カキカキする所を#ifで囲まないといけないっていう問題はあるんだけどね。
 ちょっとしたログ出力ならこれでもいいだろーと思う。
 アプリケーションに組み込んでログ出力って言ったら、当然、config作るだろうから、それが「うぜー」という場合は、そもそもログとらん。


2005.06.17

あれ?このサイトって、何のサイトだっけ?

手動トラックバック先URI
http://www.studio-odyssey.net/content/note/archive01.htm#f1705

 あれですよ。
 このサイト、プログラミングのサイトじゃないんですけど、プログラムの話ですよ。
 最近多いですな。つーか、仕事で.net使うようになったんで、共通の話題が出来たともいう。前の言語の情報なんか流したって、特に面白くもなかったしなー。

 そんなわけで、.netですよ。あれですよ、なんつーか、やっぱりやってくれたな、MS!!ですよ。

 こー、画面を作るじゃないですか。ぺたっと。
 で、そこにコントロールをぐりぐり配置するじゃないですか。C/C++の人はとりあえず置いておいて、VBとかC#とか。
 でさー、コントロールを自作するわけですよ。反復できる事とか、コントロールで吸収したいわけで。
 で、でも実際のデザイン中には実行して欲しくない関数とかがあるわけですよ。メソッドというのが正しいのかも知れないですが、ようは、デザイン時は実行して欲しくないんだけど、実行時だけやってーみたいな。

 ASP.netの場合、これ、SiteのDesignModeというプロパティで知れるとなってるんですよ。
 んだから、良く、bool isdesign = (Site != null && Site.DesignMode)っていう書き方をしていたわけです。で、これで問題なかったのですよ。今まで。
 でもこれ、プロパティに書くとダメなのな。
 保存されたページを開き直すと、プロパティが設定し直されるみたいなんですけど、その時、おかしな挙動をするのですよ。結果として、実行して欲しくないメソッドが実行される。
 デザインモードなのにぃぃ!

 まぁ、仕方がないんで、ちろちろと書き換えて、なんとか回避したんですけど、この先にまたあったわけですよ。
 Siteって、実行時にもnullになりうるのな。
 予想される動作の通りに流れてきたとしても、Siteがnullになる。なんで、実行時なのに、デザインモードとして解釈されてしまい実行されない。

 えーっと、つまり、DesignModeってプロパティ、意味ないんじゃないか?
 使う場所によっては問題ないんですけどね…だったらヘルプにくらい書けよと。いや、書いてあるかも知れないけど…

 なんだかなー、こまったなーと言うわけで、仕方がないんで、コントロールの属するPageオブジェクトから、Requestオブジェクトをとる事にした。
 この時、デザインモードだと例外が投げられるんで、例外を捕まえて、デザインモードとして判断。実行時はこれが入っているはずなんで、実行モードとして判断。
 泥臭いコードだ…
 なんとかしてくれ、.net…

 2005が11月に出るそうだが、お金貯めないとなー。

private bool IsDeginMode()
{
   if (Site != null)
      return Site.DesignMode;
   else
   {
      try
      {
         //これ、とれないと例外投げるんで
         return (this.Page.Request.Url.AbsoluteUri.Length == 0 ? true : false);
      }
      catch
      {
         return true;
      }
   }
}

2005.05.10

死ぬ

手動トラックバック先URI
http://www.studio-odyssey.net/content/note/archive01.htm#e1005

 左の膝がスゲー痛い。
 何故かわからないが、スゲー痛い。
 呪いか何かの類か!?
 とも思うが、多分、仕事のしすぎなんだろうと思う。

 .netで、ただひとつの機能を実装したかったのです。
 以下、スゲー、フツーに.netのプログラムの話なので、わからない人は回れ右。
 ごめーん。
 オデはプログラムのサイトじゃないよー。
 今週、もしかすると突然更新するかもなので、たまに見てやってください。ごめんなさい。マジごめんー。
 つーか、オデで作品発表以外で俺のすることって、それほど求められてもいないだろうよ。
 はにゃーん。

 さて、それはともかく、.netプログラム。言語はC#です。
 実装したいのは、こんな事です。IT会議室でも話題に上ってました。

  • 独自のコントロールを作って、それにコレクションエディタが開くプロパティをつけたい。
  • それは、永続的に使用できるプロパティにしたい。
  • 言うなれば、DropDownListとListItemの関係。
  • コードで追加するとか、そういうダサい実装はしない。全部プロパティでやりたい。

 えー、インターネット広しといえども、この機能を実現するための方法ってーのは、僕の探し方が悪かったのでしょうか…
 2件、近い事例があったのですが、完全なコードではなく、スゲー苦労しました。
 ぶっちゃけ、調べて開発するまで、3日かかりました。

 僕は、他の人たちに同じ苦労をしていただきたくないです。
 本当は、これ、仕事の一部なんで、コード出すのは問題なんですけど、これを実装して、どう利用するかの方が今回のメインな内容だったんで、この機能を実現するコードを以下にかきます。
 将来的に日誌なんで、データ落ちるかも知れません。
 その時は、誰か、このコードを教えてあげてください。

 この話題はIT会議室のスレッドで話題になっている問題を解決できるでしょう。
 僕はここに書き込みませんので、気づいた人はコードを持っていって、教えてあげてもよいです。コードの信頼性はわかんないです。それほどテストしてないので。

 では、以下にコードです。長いです。コメントそれなりに入れておきますが、わかんない時はMSDNで調べてください。それでもわかんない時はケイジバンにでもどうぞ。
 コードは完全なコードです。そのまま使えます。サンプルコードとはそういうものであると思うので。
 まぁ、このままだと読みにくいんで、コピペして見るといいです。

using System;
using System.Collections;
using System.ComponentModel;

//テキトーにつけたので、変えてね
namespace ClassLibrary1
{
   /// <summary>
   /// Collectionなプロパティが、エディタで開かれ、そこで追加したものがコードに書き込まれるサンプルです。
   /// 面倒なので実装はコンポーネントでしますが、WEB.UIで実装もできるでしょう。試してはいないです。
   /// これはDelった時に子の要素もちゃんと消えます。なぜなら、Itemsに対して、Addでnewしてコレクションを追加していくためです。
   /// ComponentItem item1 = new ComponentItem();
   /// CollectionProtertyComponent.Item.Add(item1);のような実装ではないです。
   /// CollectionProtertyComponent.Item.Add(new ComponentItem("aa"));のような実装です。なので、Delete時に綺麗に子どもも消えます。
   /// それは多分、あなたの求める動作です。
   /// </summary>
   public class CollectionProtertyComponent : System.ComponentModel.Component
   {
      /// <summary>
      /// コレクションです。
      /// </summary>
      private ComponentItemCollection _item;

      public CollectionProtertyComponent()
      {
         this._item = new ComponentItemCollection();
      }

      /// <summary>
      /// Itemです。
      /// ポイントはDesignerSerializationVisibilityです。
      /// </summary>
      [Description("ここに設定します")]
      [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
      [Browsable(true)]
      [Bindable(true)]
      public ComponentItemCollection Items
      {
         get
         {
            return this._item;
         }
      }
   }

   /// <summary>
   /// これが、保持されるItemの1つずつです。
   /// Serializableです。そして、TypeConverterを持ちます。それはComponentItemConverterで、自分で実装します。これがポイントです。
   /// これがないと、Delった時に問題が起こります。Delった時、子要素がコードに残るのです。
   /// </summary>
   [Serializable()]
   [TypeConverter(typeof(ComponentItemConverter))]
   public class ComponentItem
   {
      /// <summary>
      /// これはIDというstringのひとつだけを持ちます。
      /// </summary>
      private string _id;

      /// <summary>
      /// 通常のコンストラクタはあんまり意味ないです。コードで実装する時に使うです。
      /// </summary>
      public ComponentItem()
      {
         this._id = this.GetType().ToString();
      }

      /// <summary>
      /// このコンストラクタでnewしたものをコレクションにAddしてもらえばよいのです。
      /// Itemのプロパティ値が増えたら、当然増えたものがいります。そしてComponentItemConverterも修正しなければなりません。
      /// </summary>
      /// <param name="id">ID</param>
      public ComponentItem(string id)
      {
         this._id = id;
      }

      /// <summary>
      /// 唯一のプロパティです
      /// </summary>
      public string ID
      {
         get
         {
            return this._id;
         }
         set
         {
            this._id = value;
         }
      }
   }

   /// <summary>
   /// 型コンバータです。これがミソです。つーか、これがないとダメです。難しいです。MSDNを見まくってください。
   /// 基本的なoverrideはそれ自体に意味はないです。
   /// </summary>
   internal class ComponentItemConverter : ExpandableObjectConverter
   {
      public override PropertyDescriptorCollection
         GetProperties(ITypeDescriptorContext context,
         object value,
         Attribute[] filter)
      {
         return TypeDescriptor.GetProperties(value, filter);
      }

      public override bool GetPropertiesSupported(ITypeDescriptorContext context)
      {
         return true;
      }

      public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
      {
         if (sourceType == typeof(string))
            return true;
         return base.CanConvertFrom(context, sourceType);
      }

      public override object ConvertFrom( ITypeDescriptorContext context,
         System.Globalization.CultureInfo info, object value)
      {
         return base.ConvertFrom(context, info, value);
      }

      public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
      {
         if (destinationType == typeof(System.ComponentModel.Design.Serialization.InstanceDescriptor))
            return true;
         return base.CanConvertTo(context, destinationType);
      }

      public override object ConvertTo(ITypeDescriptorContext context,
         System.Globalization.CultureInfo culture, object value, Type destType)
      {
         //ここはちょっとポイントです。よく見てください。
         if (destType == typeof(string) && value is ComponentItem)
         {
            ComponentItem p = (ComponentItem)value;
            return p.ID;
         }
         if (destType == typeof(System.ComponentModel.Design.Serialization.InstanceDescriptor) && value is ComponentItem)
         {
            ComponentItem sc = (ComponentItem)value;
            //ここは実装に合わせて追加する必要があるでしょう。
            //これがあることによって、items.Add(new ComponentItem("AA"));
            //のようなコードが埋め込まれます。こうしておかないと、ただシリアライズしただけだとItemの内容がnewされて残るので、
            //Delったときに子要素がコードに残ってださいのです。つーか、それはいらないので一緒に消えて欲しいはずですから。
            System.Reflection.ConstructorInfo ctor = typeof(ComponentItem).GetConstructor(new Type[] {typeof(string)});
            if (ctor != null)
            {
               //ここも追加するように
               return new System.ComponentModel.Design.Serialization.InstanceDescriptor(ctor, new object[] {sc.ID});
            }
         }
         return base.ConvertTo(context, culture, value, destType);
      }
   }

   /// <summary>
   /// これはComponentItemをコレクションにしたものです。コレクションの方が扱いやすいので、コレクションです。
   /// コレクションの基本的な実装のコメントはつけないので、CollectionBaseを参照してください。
   /// </summary>
   [Serializable()]
   public class ComponentItemCollection: System.Collections.CollectionBase
   {
      /// <summary>
      /// インデクサの実装です
      /// </summary>
      public ComponentItem this[ int index ]
      {
         get
         {
            return ( (ComponentItem) List[index] );
         }
         set
         {
            List[index] = value;
         }
      }

      /// <summary>
      /// これは必要です。これで実装されます。
      /// </summary>
      /// <param name="value">ComponentItem</param>
      /// <returns>int</returns>
      public int Add( ComponentItem value )
      {
         return( List.Add( value ) );
      }

      public void Insert( int index, ComponentItem value )
      {
         List.Insert( index, value );
      }

      public void Remove( ComponentItem value )
      {
         List.Remove( value );
      }

      public bool Contains( ComponentItem value )
      {
         // If value is not of type Schedule, this will return false.
         return( List.Contains( value ) );
      }

      //このあたり、typeOfがあるんで、注意
      protected override void OnInsert( int index, Object value )
      {
         if ( value.GetType() != Type.GetType("ClassLibrary1.ComponentItem") )
            throw new ArgumentException( "value must be of type ComponentItem. (" + value.GetType().ToString() + ")" , "value" );
      }

      protected override void OnRemove( int index, Object value )
      {
         if ( value.GetType() != Type.GetType("ClassLibrary1.ComponentItem") )
            throw new ArgumentException( "value must be of type ComponentItem. (" + value.GetType().ToString() + ")" , "value" );
      }

      protected override void OnSet( int index, Object oldValue, Object newValue )
      {
         if ( newValue.GetType() != Type.GetType("ClassLibrary1.ComponentItem") )
            throw new ArgumentException( "newValue must be of type ComponentItem. (" + newValue.GetType().ToString() + ")", "newValue" );
      }

      protected override void OnValidate( Object value )
      {
         if ( value.GetType() != Type.GetType("ClassLibrary1.ComponentItem") )
            throw new ArgumentException( "value must be of type ComponentItem. (" + value.GetType().ToString() + ")" );
      }
   }
}

 論より証拠のなんとかなので、「もしや、これぞわたしの求めていた物では?」と思ったら、コピペって試しに動かしてみるとよいです。コードは少し直す必要はあるでしょうけど。

 おそらく、「実現したいこと」はこの応用で扱えると思います。WEB開発であってもです。そもそも、これはWEB開発で使う予定で作っています。aspx側にタグは挿入されませんが(コンポーネントですから)、基本に立ち返って、「タグは必要か?」と考えてみるのもよいでしょう。そして、「必要だ」と思ったとき、あなたはDropDownListのような入れ子で表現したいと思うかも知れません。ですが、それは「本当にaspページの部品として、入れ子になっていなければならないか?」を考えてください。あなたがプロパティでウィンドウを開けるようにしたのは、利用者に「タグうちしてもらいたくない」からだったと思います。もしも「タグを打つ」のが必要ならば、「コードでAddする」のも意味は同じはずです。

 これを作るのに3日とかかかった僕はまだまだだにゃあと思う。


2005.05.05

私的メモ

手動トラックバック先URI
http://www.studio-odyssey.net/content/note/archive01.htm#e0505

<ObsoleteAttribute("This function will be removed from future Versions.Use another function 'NewFunction'", False)> _

 VB.netでメソッドを「使わないでねー」と警告する(エラーではない)時にメソッドの上につけるAttribute。
 これを関数宣言のトコにかくと、コンパイラが警告を出す。

 結論。

 全部出して、出なくなるまで潰す。

 多分、それが一番速いんじゃないかなーと思うんだけど、どうかなー。

 納品するものは一通り終わって、新規のコントロール作り。
 いろいろ考えることが多いんだが、この考えている時が一番楽しいなあと思う自分は、やっぱりプログラム好きなんだなあと思う。でも、コードをかくのはあんまり好きじゃないから、コーダではない。