<   2011年 06月 ( 26 )   > この月の画像一覧
(.Net)メッセージボックスを最前面に表示したい
現在 BackgroundWorker コンポーネントを使ったマルチスレッドアプリケーションを使ってます。
メインスレッドはフォームのUIを表示し、別スレッドで動くBackgroundWorker.DoWorkイベントハンドラで時間のかかる処理をしています。

この別スレッド側でメッセージボックスを表示したときに、メインスレッドのフォームUIより手前に表示したい かつ 最前面に表示というのが今回の要件です。
(ちなみに、フォームの TopMost プロパティが true でも最前面に表示になるので、メッセージボックスをお手製で作って TopMost プロパティを使うという方法もとれるかもしれません。)

MessageBox クラスには IWin32Window を指定することで、指定したオブジェクより前面に表示する Show メソッドがありますが、今回は最前面に表示したいので、その方法は使いませんでした。

で、調べると、MessageBoxOptions 列挙体MessageBox.Show メソッドに指定することによって最前面に表示ができるようです。

MessageBoxOptions 列挙体で、DefaultDesktopOnly か ServiceNotification を指定したら最前面に表示されました。他のウィンドウに隠れることはありません。
ただし、visual スタイルを使用できないで、古臭いUIにはなります。

DefaultDesktopOnly と ServiceNotification の違いですが、DefaultDesktopOnlyでは既定のデスクトップ(ユーザーがログオンした後でアプリケーションが動作するデスクトップ)でのみ表示され、ServiceNotificationはログインしているユーザーがいない場合でもメッセージボックスが表示されます。
ServiceNotificationは主に、サービスからのメッセージボックス表示に使われるようです。

ユーザログインしてから使うアプリケーションから表示するものだったので、DefaultDesktopOnly を指定しました。

テストソースは下記のような感じです。(C#)

MessageBox.Show("test",
"caption",
MessageBoxButtons.OKCancel,
MessageBoxIcon.Information,
MessageBoxDefaultButton.Button1,
MessageBoxOptions.DefaultDesktopOnly);



参考:
メッセージボックスを表示する: .NET Tips: C#, VB.NET, Visual Studio
Program.csでメッセージボックスを最前面に出す方法
[C#]同じこと考える人いるんですね - Program.csでメッセージボックスを最前面に出す方法
[PR]
by jehoshaphat | 2011-06-30 22:29 | .Net開発 | Trackback | Comments(0)
(.Net)File.Create(ファイル名)でファイルを作成したら必ずCloseしよう

.Net で File.Create() メソッドを使うと簡単にファイルの作成ができます。
しかし、上記MSDNにあるように、元のファイルのハンドルが閉じられるまで、作成されたファイルに他のプロセスやコードからアクセスすることはできないので、ハンドルを閉じてやる必要があります。

こんな感じです。

FileStream fs = File.Create(Path.Combine(@"c:\test","test.txt"));
fs.Close();

ただ単に空ファイルを作るだけなら下記のようにもできます。
File.Create(Path.Combine(@"c:\test","test.txt")).Close();

余談ですが、Path.Combine ってパスを作成するときに便利ですね。

参考:
C#でFile.Create(filename)したままだと、ファイルが開いたままになる
[PR]
by jehoshaphat | 2011-06-30 00:40 | .Net開発 | Trackback | Comments(0)
(.Net)Windows共有フォルダのセッションの切断する
.Net アプリケーションからWindows共有フォルダのセッションを切断する方法です。
GUIだと、コンピュータの管理 から 共有フォルダ→セッション→セッション右クリック→セッションを閉じる ということを .Net アプリケーションからしたいわけです。

コマンドプロンプトから、net session \\コンピュータ名 /delete でセッションを切断することができるので、当初 .Net アプリからこのコマンドをキックしようと思ったのですが、これが自身のPCにしかできません。また、コンピュータ名での指定しかできません。
今回の要件は .Net 側からリモートのファイルサーバの共有フォルダセッションを解除したいということです。(つまり、.NETアプリが動くPCとファイルサーバが別コンピュータ)。そして、ユーザのほとんどはターミナルサーバからログインしており、特定のユーザだけ削除したいのです。

そこで、結局 Windows API の NetSessionDel 関数を使うことにしました。このAPIだとリモートのコンピュータに対して、ユーザのPC名、ユーザ名を指定してセッション切断ができます。

下記のような感じでセッション切断できます。(C#)

/// <summary>
/// セッションを閉じる
/// </summary>
/// <param name="strServerName">サーバ名(\\servernameの形式) null なら自身を表す。</param>
/// <param name="strClientHostName">クライアントホスト名(\\servernameの形式)NULL を指定すると、username パラメータで指定されたユーザーのすべてのセッションは、servername パラメータで指定されたサーバーから削除</param>
/// <param name="strUserName">ユーザ名。NULL を指定すると、UncClientName パラメータで指定されたクライアント名に関係する、すべてのユーザーのセッションが終了</param>
/// <returns>Win32エラーコード</returns>
public static NET_API_STATUS CloseSession(string strServerName, string strClientHostName, string strUserName) {
StringBuilder strBulSvname = null ;
if (! String.IsNullOrEmpty(strServerName) )
strBulSvname = new StringBuilder(strServerName);
 
StringBuilder strBulClientHostName = null;
if ( ! String.IsNullOrEmpty(strClientHostName))
strBulClientHostName= new StringBuilder(strClientHostName);
 
StringBuilder strBulUserName = null;
if ( ! String.IsNullOrEmpty(strUserName))
strBulUserName = new StringBuilder(strUserName);
 
NET_API_STATUS res = NetSessionDel(strBulSvname, strBulClientHostName, strBulUserName);
 
Console.WriteLine(res.ToString());
return res;
 
}
 
//WindowsAPI使用定義
[DllImport("netapi32.dll" ,CharSet= CharSet.Unicode)]
private static extern NET_API_STATUS NetSessionDel(
StringBuilder servername,
StringBuilder UncClientName,
StringBuilder username);
/// <summary>
/// Lmcons.h
/// #define NET_API_STATUS DWORD
/// </summary>
public enum NET_API_STATUS : uint {
NERR_Success = 0,
/// <summary> This computer name is invalid.</summary>
NERR_InvalidComputer = 2351,
/// <summary>This operation is only allowed on the primary domain controller of the domain.</summary>
NERR_NotPrimary = 2226,
/// <summary>This operation is not allowed on this special group.</summary>
NERR_SpeGroupOp = 2234,
/// <summary>This operation is not allowed on the last administrative account.</summary>
NERR_LastAdmin = 2452,
/// <summary>The password parameter is invalid.</summary>
NERR_BadPassword = 2203,
/// <summary>The password does not meet the password policy requirements. Check the minimum password length, password complexity and password history requirements.</summary>
NERR_PasswordTooShort = 2245,
/// <summary> The user name could not be found.</summary>
NERR_UserNotFound = 2221,
 
ERROR_ACCESS_DENIED = 5,
ERROR_NOT_ENOUGH_MEMORY = 8,
ERROR_INVALID_PARAMETER = 87,
ERROR_INVALID_NAME = 123,
ERROR_INVALID_LEVEL = 124,
ERROR_MORE_DATA = 234,
ERROR_SESSION_CREDENTIAL_CONFLICT = 1219
}



NET_API_STATUS res にはNetSessionDelの戻りのWindowsエラーコードが返ります。
エラーコードのメッセージを取得するには、(.Net)WindowsAPIのエラーコードからメッセージを取得するを参照してください。

注意点として、切断対象とするクライアントホスト名ですが、コンピュータの管理のセッションで出てくるコンピュータ名じゃないとダメなようです。(ここがコンピュータ名とIPアドレスとの時があるので。。。)
ですので、(.Net)共有フォルダにアクセスしているセッションを取得するでセッション情報を取ってきて、それのComputerNameの値を使うのがいいかもしれません。
また、NetSessionDelの引数を全てNULLにするとエラーになりました。クライアントコンピュータ名かユーザ名かはどちらかは指定しないといけないのかもしれません。

MSDNによると「Administrators または Account Operators ローカルグループのメンバだけがこの関数を実行できる」とのことです。
一般ユーザで使う場合は(.Net)別ユーザでプロセスを起動するで書いたようにAdministrator権限でmsgをキックするようにするといいと思います。



参考:
net sessionコマンドでパソコンに接続しているセッションを切断する :ITpro net sessionコマンドで切断する場合です。
[PR]
by jehoshaphat | 2011-06-30 00:34 | .Net開発 | Trackback | Comments(0)
(.Net)共有フォルダにアクセスしているセッションを取得する
Windows共有フォルダにアクセスしているセッション情報を .NET アプリケーションから取得する方法です。
クライアントPCから実行し、共有フォルダを提供しているサーバPCのセッションを取得します。

コマンドでするなら、net session で取得することができますが、共有フォルダを提供しているコンピュータでしか実行できません。また、整形して出力するので長いユーザ名だと切れてしまいそうです。(クライアントは文字列途中で切れてます)

ということで WMI で取得することにしました。

WMI Code Creator を使うと簡単にセッション取得のコードが生成できます。

C#だと下記のような感じです。

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Management;
 
namespace WMISample{
public class MyQuerySample : System.Windows.Forms.Form{
private System.Windows.Forms.Label userNameLabel;
private System.Windows.Forms.TextBox userNameBox;
private System.Windows.Forms.TextBox passwordBox;
private System.Windows.Forms.Label passwordLabel;
private System.Windows.Forms.Button OKButton;
private System.Windows.Forms.Button cancelButton;

private System.ComponentModel.Container components = null;
 
public MyQuerySample(){
InitializeComponent(); }
 
protected override void Dispose( bool disposing ) {
if( disposing ) {
if (components != null) {
components.Dispose(); }
}
base.Dispose( disposing );
}
 
private void InitializeComponent() {
//省略
}
 
[STAThread]
static void Main() {
Application.Run(new MyQuerySample());
}
 
private void OKButton_Click(object sender, System.EventArgs e) {
try {
ConnectionOptions connection = new ConnectionOptions();
connection.Username = userNameBox.Text;
connection.Password = passwordBox.Text;
connection.Authority = "ntlmdomain:hogedomain";
 
ManagementScope scope = new ManagementScope(
"\\\\fileserver\\root\\CIMV2", connection);
scope.Connect();
 
ObjectQuery query= new ObjectQuery(
"SELECT * FROM Win32_ServerConnection");
 
ManagementObjectSearcher searcher =
new ManagementObjectSearcher(scope, query);
 
foreach (ManagementObject queryObj in searcher.Get()){
Console.WriteLine("-----------------------------------");
Console.WriteLine("Win32_ServerConnection instance");
Console.WriteLine("-----------------------------------");
Console.WriteLine("ActiveTime: {0}", queryObj["ActiveTime"]);
Console.WriteLine("Caption: {0}", queryObj["Caption"]);
Console.WriteLine("ComputerName: {0}", queryObj["ComputerName"]);
Console.WriteLine("ConnectionID: {0}", queryObj["ConnectionID"]);
Console.WriteLine("Description: {0}", queryObj["Description"]);
Console.WriteLine("InstallDate: {0}", queryObj["InstallDate"]);
Console.WriteLine("Name: {0}", queryObj["Name"]);
Console.WriteLine("NumberOfFiles: {0}", queryObj["NumberOfFiles"]);
Console.WriteLine("NumberOfUsers: {0}", queryObj["NumberOfUsers"]);
Console.WriteLine("ShareName: {0}", queryObj["ShareName"]);
Console.WriteLine("Status: {0}", queryObj["Status"]);
Console.WriteLine("UserName: {0}", queryObj["UserName"]);
}
Close();
}catch(ManagementException err) {
MessageBox.Show("An error occurred while querying for WMI data: " + err.Message);
}catch(System.UnauthorizedAccessException unauthorizedErr) {
MessageBox.Show("Connection error (user name or password might be incorrect): " + unauthorizedErr.Message);
}
}
 
private void cancelButton_Click(object sender, System.EventArgs e) {
Close();
}
}
}


VBScriptだと下記のようになります。(文字数制限の関係でハイライトOFFです。)

strComputer = "fileserver"
strDomain = "hogedomain"
Wscript.StdOut.Write "Please enter your user name:"
strUser = Wscript.StdIn.ReadLine
Set objPassword = CreateObject("ScriptPW.Password")
Wscript.StdOut.Write "Please enter your password:"
strPassword = objPassword.GetPassword()
Wscript.Echo

Set objSWbemLocator = CreateObject("WbemScripting.SWbemLocator")
Set objWMIService = objSWbemLocator.ConnectServer(strComputer, _
"root\CIMV2", _
strUser, _
strPassword, _
"MS_409", _
"ntlmdomain:" + strDomain)
Set colItems = objWMIService.ExecQuery( _
"SELECT * FROM Win32_ServerConnection",,48)
For Each objItem in colItems
Wscript.Echo "-----------------------------------"
Wscript.Echo "Win32_ServerConnection instance"
Wscript.Echo "-----------------------------------"
Wscript.Echo "ActiveTime: " & objItem.ActiveTime
Wscript.Echo "Caption: " & objItem.Caption
Wscript.Echo "ComputerName: " & objItem.ComputerName
Wscript.Echo "ConnectionID: " & objItem.ConnectionID
Wscript.Echo "Description: " & objItem.Description
Wscript.Echo "InstallDate: " & objItem.InstallDate
Wscript.Echo "Name: " & objItem.Name
Wscript.Echo "NumberOfFiles: " & objItem.NumberOfFiles
Wscript.Echo "NumberOfUsers: " & objItem.NumberOfUsers
Wscript.Echo "ShareName: " & objItem.ShareName
Wscript.Echo "Status: " & objItem.Status
Wscript.Echo "UserName: " & objItem.UserName
Next


参考:
net sessionコマンド - 管理者必見! ネットワーク・コマンド集:ITpro net session コマンドの説明です。
[WMI] 共有フォルダのセッションを取得するサンプル: Win32_ServerConnection - WMI Sample
[WMI] Win32_ServerConnection クラス - WMI Library
@IT:Win32 APIやDLL関数を呼び出すには? Win32APIを呼び出す方法です。
[PR]
by jehoshaphat | 2011-06-07 00:04 | .Net開発 | Trackback | Comments(0)
(.Net)特定のターミナルサーバ上のユーザにメッセージを送信する
.Netアプリケーションから任意のWindowsユーザにメッセージを送信する方法です。

他のコンピュータやターミナルサービス利用中のユーザにメッセージを送信するには msg コマンドが使えます。
(学生時代 net send というコマンドで他のWindowsPCにメッセージを送れることを知ってよく遊んでたんですが、今は net send コマンドの代わり msg コマンドを使うようです。Vista以降は net send コマンド自体が使えないようですし。。)

ちなみに、Windows7やVistaではターミナルサービスセッションに対するリモートからのRPC接続が無効に設定されているようなので、普通にはメッセージを送れないようです。
クライアントPCのレジストリで HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server キーの AllowRemoteRPC の値を 1 にするとRPC接続が有効にされれるようです。このあたりの詳しい話は【Management】Msgコマンドを使用すると「セッション名の取得エラー5」 - フィールドSEあがりの安納ですが参考になります。


msg コマンドは、ユーザ or セッションID or セッション名 でどのセッションに送信するかや、どのコンピュータに送信するかなどが指定できます。(どのコンピュータに送るかについて、ホスト名でなくてもIPアドレスでもOKでした。)
ただし、そのセッションに対するアクセス権を持っていないと「エラー [5]:アクセスが拒否されました。」となってしまいます。
まぁドメイン管理者権限なら大抵大丈夫なはずです。

msg コマンドでデフォルトの設定で送信すると、どうやら60秒間しかメッセージが表示されませんでした。ということで、1分では足りない場合は /time:秒数 で指定した方がよさそうです。

また、msgコマンドでアクセス権等不足してたり送った先のユーザが既にログアウトしてたりとエラーがあった場合ですが、どうやらエラーがあってもプロセスの戻り値は0のようです。
なので、エラーがあったかどうかは標準エラー出力からとらないとわかりません。
で、.Netでコンソールアプリケーションの標準出力や標準エラー出力を取得する方法ですが、Process.StandardError プロパティで標準エラー出力、Process.StandardOutput プロパティで標準出力の値が取れるようです。
ただし、Process.StandardError プロパティにアクセスするには ProcessStartInfo.RedirectStandardError を true に、Process.StandardOutput プロパティにアクセスするには、ProcessStartInfo.RedirectStandardOutput を true にしないといけないようです。
(上記の方法は同期式の方法と呼ばれており、処理中プロセスの標準出力を取ろうとしたりするとデッドロックになってしまう可能性があるようです。その場合はイベントハンドラを設定する非同期式を使うことが勧められています。)


ということで下記が.Netからmsgコマンドをキックしてメッセージを送信するコードです。コマンドプロンプトウィンドウは表示しないようにしてます。(C#)

/// <summary>
/// メッセージを送信する
/// </summary>
/// <param name="strUserName">ユーザ名</param>
/// <param name="strHostName">コンピュータ名orIPアドレス</param>
/// <param name="strMsg">メッセージ</param>
/// <param name="iDispTime">メッセージ表示秒数</param>
/// <returns>エラーメッセージ(nullの場合は正常)</returns>
public static string SendMessage(string strUserName, string strHostName, string strMsg, int iDispTime){
//プロセスオブジェクト生成
Process proc = new Process();
// 起動するアプリケーションを設定する
proc.StartInfo.FileName = "msg.exe";
//引数指定
proc.StartInfo.Arguments = string.Format("{0} /server:{1} /time:{2} {3} ", strUserName, strHostName, iDispTime.ToString() , strMsg);
// ウィンドウを作成しないかどうかを設定する (初期値 false)trueにするとウィンドウ表示なしとなる
proc.StartInfo.CreateNoWindow = true;
// シェルを使用するかどうか設定する (初期値 true)falseにするとコンソールウィンドウが表示なしとなる
proc.StartInfo.UseShellExecute = false;
// 起動できなかった時にエラーダイアログを表示するかどうかを設定する (初期値 false)
proc.StartInfo.ErrorDialog = false;
//標準エラー出力を取得できるようにする。
proc.StartInfo.RedirectStandardError = true;
 
// 起動する
proc.Start();
//起動したプロセスが終了するまで待つ。1秒のみ
proc.WaitForExit(1000);
 
//標準エラー出力取得
if (proc.HasExited){
string strErr = proc.StandardError.ReadToEnd();
strErr = strErr.Replace("\r\n", "");
if (String.IsNullOrEmpty(strErr)){
return null; //正常終了
}
return strErr; //異常終了 エラーメッセージ返す
}
return "タイムアウトしました。";
}

ただドメインのAdministrator以外だとアクセス権の問題で送信できないことがあるので、管理者ユーザ以外で使う場合は(.Net)別ユーザでプロセスを起動するで書いたようにAdministrator権限でmsgをキックするようにするといいと思います。

参考:
msg - Windowsコマンド集:ITpro
[Terminal Server コマンド] MSG
【Windows7/Vista】ポップアップメッセージを送りたい(net send あらため msg.exe) - フィールドSEあがりの安納です - Site Home - TechNet Blogs
コマンド・プロンプトでファイル共有を管理する - @IT コマンドで管理する場合の情報です。
DOS「メッセージ送信」コマンド(Hishidama's Windows DOS command "msg" Memo)
[.NET] コンソールアプリケーションの標準入出力をリダイレクトする - あおいるかの倉庫
[PR]
by jehoshaphat | 2011-06-06 23:42 | .Net開発 | Trackback | Comments(0)
(.Net)LINQでラムダ式を動的に生成する
(.Net)LINQのクエリ構文とメソッド構文(ラムダ式)を使ってみた で LINQ で動的なクエリを生成する方法がわからんと言っていましたが、いろいろ参考サイトを巡回した結果、よーやく動的クエリをある程度なら処理できる仕組みを作ることができました。
任意のクラスのコレクションで、複数の条件を動的に処理するというものです。(現段階では論理演算子はネストできず、ANDかORしか選べないのでSQLほど複雑なクエリはできません。)

方法はラムダ式を動的に生成するというものです。C#3.0(.NET Framework 3.5)

まず、呼び出し元側のソースです。TestClassのコレクションをLINQで動的ラムダ式を使って検索します。(文字数の関係でハイライトはOFFです。)

public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

private void button1_Click(object sender, EventArgs e)
{
LamdaUtility lamda = new LamdaUtility();

//テスト用のデータ生成
List<TestClass> dataSource = new List<TestClass>();
dataSource.Add(new TestClass() { Name = "Asou Kumiko", Id = 1, TrueFalse = false });
dataSource.Add(new TestClass() { Name = "asou kumiko", Id = 2, TrueFalse = true });
dataSource.Add(new TestClass() { Name = "Mikazuki Shizuka", Id = 3, TrueFalse = true });
dataSource.Add(new TestClass() { Name = "mikazuki shizuka", Id = 1, TrueFalse = true });
dataSource.Add(new TestClass() { Name = "Kiriyama", Id = 5, TrueFalse = false });
dataSource.Add(new TestClass() { Name = "JikouKeisatu", Id = 4, TrueFalse = false });
dataSource.Add(new TestClass() { Name = "", Id = 6, TrueFalse = false });
dataSource.Add(new TestClass() { Name = null, Id = 7, TrueFalse = false });

Console.WriteLine("動的ラムダ結果");
//条件クリア
lamda.ClearQuery();
//IDの条件設定 1,7 が対象
lamda.AddQuery(LamdaUtility.QueryTypeEnum.Equal, "Id", 1);
lamda.AddQuery(LamdaUtility.QueryTypeEnum.Equal, "Id", 7);
//Nameの条件設定 "zuka"を含むもの
lamda.AddQuery(LamdaUtility.QueryTypeEnum.StringContainsIgnoreCase, "Name", "zuka");


//論理条件
//lamda.AndOr = LamdaUtility.AndOrTypeEnum.AND;
lamda.AndOr = LamdaUtility.AndOrTypeEnum.OR;

//上記条件を普通にラムダ式使うと下記のようになる

//var list = from p in dataSource
// where p.Id == 1 || p.Id == 7 || p.Name.ToLower().Contains("zuka")
// select p;

// Expression Tree を使って条件を組み立てる
var list = dataSource.AsQueryable().
Where(GetPredicate(lamda));

/*下記のラムダ式クエリでしたい場合は、GetPredicateメソッドの戻り値をコメントアウトしてる方にすればよい
var list = from p in dataSource
where GetPredicate(lamda)(p)
select p;
*/

//フィルタを実行
List<TestClass> resList = list.ToList();

//抽出結果表示
resList.ForEach(p => Console.WriteLine(p.Name + " " + p.Id.ToString()));
}

/// <summary>条件ラムダ式を生成する</summary>
/// <returns></returns>
public static Expression<Func<TestClass, bool>> GetPredicate(LamdaUtility lmd)
{
// パラメータ生成
ParameterExpression param = Expression.Parameter(typeof(TestClass), "p");
//右辺を生成
Expression body = lmd.GetPredicate(param);
// パラメータと本体をくっつけて、実行コード生成
//return Expression.Lambda<Func<Person, bool>>(body, param).Compile(); //コンパイルしたものを返す場合はメソッド定義の"Expression"を消す必要あり
return Expression.Lambda<Func<TestClass, bool>>(body, param);
}

}

public class TestClass
{
public string Name { get; set; }
public int Id { get; set; }
public bool TrueFalse { get; set; }
}


そして下記がラムダ式の右辺(条件部分)を動的に作成するクラスです。このクラスをDLLにしとくと便利かもしれません。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Reflection;

namespace LamdaUtil
{
/// <summary>動的クエリのためのラムダ式を生成するコアクラス</summary>
public class LamdaUtility
{
/// <summary>列挙型:比較方法</summary>
public enum QueryTypeEnum
{
/// <summary>比較演算子(等しい) ==</summary>
Equal = 0,
/// <summary>比較演算子(等しくない) !=</summary>
NotEqual = 1,
/// <summary>String.Containsを使って文字列を含むか検索(大文字小文字無視)</summary>
StringContainsIgnoreCase = 6,
/// <summary>String.Containsを使って文字列を含むか検索(大文字小文字を判断)</summary>
StringContains = 7,
/// <summary>比較演算子(小なり) <</summary>
LessThan = 2,
/// <summary>比較演算子(大なり) ></summary>
GreaterThan = 3,
/// <summary>比較演算子(小なりイコール) <=</summary>
LessThanOrEqual = 4,
/// <summary>比較演算子(大なりイコール) >=</summary>
GreaterThanOrEqual = 5,

}

/// <summary>列挙型:複数条件の場合にANDかORか(設定しないとORになる。OR=0のため)</summary>
public enum AndOrTypeEnum
{
/// <summary>複数条件の場合、AND条件とする</summary>
AND = 1,
/// <summary>複数条件の場合、OR条件とする</summary>
OR = 0
}

/// <summary>インナークラス:クエリ条件を保存する</summary>
class LamdaUtilityQuery
{
/// <summary>比較方法</summary>
public LamdaUtility.QueryTypeEnum QueryType { get; set; }
/// <summary>比較対象の項目(フィールド)名</summary>
public string ItemName { get; set; }
/// <summary>比較条件</summary>
public object Keyword { get; set; }
}

/// <summary>String.Containsメソッド(文字列検索に利用)</summary>
private readonly MethodInfo Contains = typeof(string).GetMethod("Contains");

/// <summary>String.ToLowerメソッド(文字列検索に利用)</summary>
private readonly MethodInfo ToLower = typeof(string).GetMethod("ToLower", Type.EmptyTypes);


/// <summary>クエリ条件のリスト</summary>
private List<LamdaUtilityQuery> mQueryKeywod = new List<LamdaUtilityQuery>();

/// <summary>検索条件の論理演算子(設定しないとORになる)</summary>
public AndOrTypeEnum AndOr { get; set; }

/// <summary>条件を追加する</summary>
/// <param name="type">比較方法</param>
/// <param name="itemName">比較対象の項目名</param>
/// <param name="keyword">比較条件</param>
public void AddQuery(QueryTypeEnum type, string itemName, object keyword)
{
mQueryKeywod.Add(new LamdaUtilityQuery() { QueryType = type, ItemName = itemName, Keyword = keyword });
}

/// <summary>条件をすべてクリアする</summary>
public void ClearQuery()
{
mQueryKeywod.Clear();
}

/// <summary>設定されたクエリ条件からラムダ式(右辺のみ)を生成する</summary>
/// <param name="param">パラメータ(比較対象クラス)</param>
/// <returns>右辺のラムダ式</returns>
public Expression GetPredicate(ParameterExpression param)
{
//条件が設定されてない時
if (this.mQueryKeywod.Count <= 0)
{
//リスト内全てをヒットしたことにするための苦肉の策(もっといい方法はないか?)
Expression orExpr = Expression.Or(
Expression.Constant(true),
Expression.Constant(true)
);
return orExpr;
}

// 条件式木のリスト
var predList = new List<Expression>();
//検索条件クラスリストをループ
this.mQueryKeywod.ForEach(q =>
{
predList.Add(GetExpressionOperator(q.QueryType, param, q.ItemName, q.Keyword));
});

//論理演算子を設定
ExpressionType exType = ExpressionType.OrElse;
switch (this.AndOr)
{
case AndOrTypeEnum.AND:
exType = ExpressionType.AndAlso;
break;
case AndOrTypeEnum.OR:
exType = ExpressionType.OrElse;
break;
default:
break;
}

//右辺式の組み立て
var body = predList.Aggregate(
(l, r) => Expression.MakeBinary(exType, l,r));
return body;
}

/// <summary>比較方法によって条件式を設定</summary>
/// <param name="type">比較条件</param>
/// <param name="param">パラメータ</param>
/// <param name="itemName">項目名</param>
/// <param name="keyword">条件</param>
/// <returns>式</returns>
private Expression GetExpressionOperator(QueryTypeEnum type, ParameterExpression param, string itemName, object keyword)
{
Expression body = null;
//左辺は項目名
var left = Expression.PropertyOrField(param, itemName);
switch (type)
{
case QueryTypeEnum.Equal:
body = Expression.Equal(left, Expression.Constant(keyword));
break;
case QueryTypeEnum.NotEqual:
body = Expression.NotEqual(left, Expression.Constant(keyword));
break;
case QueryTypeEnum.GreaterThan:
body = Expression.GreaterThan(left, Expression.Constant(keyword));
break;
case QueryTypeEnum.GreaterThanOrEqual:
body = Expression.GreaterThanOrEqual(left, Expression.Constant(keyword));
break;
case QueryTypeEnum.LessThan:
body = Expression.LessThan(left, Expression.Constant(keyword));
break;
case QueryTypeEnum.LessThanOrEqual:
body = Expression.LessThanOrEqual(left, Expression.Constant(keyword));
break;
case QueryTypeEnum.StringContainsIgnoreCase:
var keywordValue = Expression.Constant(keyword, typeof(string));
body = Expression.Call(
Expression.Call(left, ToLower),
Contains,
Expression.Call(keywordValue, ToLower));
//左辺がNULLの場合、NULL例外が発生するので、左辺はNULLで無いフィルタを設定。
body = Expression.AndAlso(
Expression.NotEqual(left, Expression.Constant(null)),
body);
break;
case QueryTypeEnum.StringContains:
var keywordValueStringContains = Expression.Constant(keyword, typeof(string));
body = Expression.Call(
left,
Contains,
keywordValueStringContains);
//左辺がNULLの場合、NULL例外が発生するので、左辺はNULLで無いフィルタを設定。
body = Expression.AndAlso(
Expression.NotEqual(left, Expression.Constant(null)),
body);
break;
default:
break;
}
return body;
}
}

}


ほんとは、最終的な式ツリーを作成する GetPredicate() メソッドも LamdaUtility クラスに入れたかったのですが、式を生成する Expression.Lambda の引数に型をしていしないといけないんですよね。
この型(今回はTestClass)は動的に変わると思ったので、呼び出し元で書くようにしました。ここをもっと汎用化できるとうれしいのですが。。。


式ツリー理解してこれ書くのにかなり時間を費やしてしまいました。。。

参考:
more Dynamic LINQ - 当面C#と.NETな記録 一番の参考になりました。感謝です。
LINQ文で動的にWhere句を組み立てるには?[3.5、C#、VB] - @IT stringのコレクションを扱うとかいった比較的単純なコレクション処理に役立ちます。
MSDN:方法 : 式ツリーを使用して動的クエリをビルドする
LINQを活用した簡単XMLデータベース・アプリケーション - @IT
Expressionを使った動的なOR文の生成 - Programmable Life
式木 - C#にハマってみる日記 Expression の演算子が載っているので参考になります。
[PR]
by jehoshaphat | 2011-06-06 23:24 | .Net開発 | Trackback | Comments(2)
(.Net)LINQのクエリ構文とメソッド構文(ラムダ式)を使ってみた
.Net Framework 3.5(C#3.0)から追加されたLINQですが、ようやく使ってみることにしました。

SQLServer等のDBへの問い合わせに使えるようですが、今回はコレクションでの検索がメインです。

とりあえず基本的なことは、@IT:LINQ(リンク)の基礎知識と、VB はじめてのLINQ - LINQ, From, Where, Select, OfTypeで頭に詰め込みます。

単純なコレクション検索だと下記のような感じになります。

public partial class Form1 : Form {
private void button1_Click(object sender, EventArgs e) {
//テスト用のデータ生成
List<TestClass> lst = new List<TestClass>();
lst.Add(new TestClass() { Name = "a", Id = 1 });
lst.Add(new TestClass() { Name = "b", Id = 2 });
lst.Add(new TestClass() { Name = "c", Id = 3 });
lst.Add(new TestClass() { Name = "d", Id = 5 });
lst.Add(new TestClass() { Name = "e", Id = 5 });
lst.Add(new TestClass() { Name = "f", Id = 4 });
lst.Add(new TestClass() { Name = "g", Id = 5 });
 
//LINQで検索 IDが5のものを探す
var query = from res in lst
where res.Id == 5
select res; //今回は TestClass オブジェクトを返すが、res.Name とかすることも可能
//foreach句で結果が参照できる
foreach (var res in query)
{
Console.WriteLine(res.Name + " , " + res.Id);
}
}
}
 
 
public class TestClass {
public string Name { get; set; }
public int Id { get; set; }
}

from in 句で検索元データを指定し、where で条件、select で結果として欲しいオブジェクトを指定します。
SQLチックで簡単にデータ抽出できますね。


しかし、いざ実用しようかと思うと、問題になったのがクエリの条件が動的になる場合です。
SQLだと string 結合するだけでよかったんですが、上記のようなLINQの書き方では動的には条件設定ができません。

LINQを活用した簡単XMLデータベース・アプリケーションにて、動的に条件を設定する方法が書いてあるんですが、ラムダ式やら式ツリーやらいまいちわかりません。


ということでLINQ 超入門(4) クエリの文法 where句 - 気楽なC#工房を参考にしました。

実際の条件に一致するかどうかのフィルタ部分を別メソッドに持たせる方法です。
こんな感じです。

public partial class Form1 : Form {
private void button1_Click(object sender, EventArgs e) {
//テスト用のデータ生成
List<TestClass> lst = new List<TestClass>();
lst.Add(new TestClass() { Name = "a", Id = 1 });
lst.Add(new TestClass() { Name = "b", Id = 2 });
lst.Add(new TestClass() { Name = "c", Id = 3 });
lst.Add(new TestClass() { Name = "d", Id = 5 });
lst.Add(new TestClass() { Name = "e", Id = 5 });
lst.Add(new TestClass() { Name = "f", Id = 4 });
lst.Add(new TestClass() { Name = "g", Id = 5 });
 
//LINQで検索 IDが5のものを探す
var query = from res in lst
where Filter(res,null,5)
select res;
//foreach句で結果が参照できる
foreach (var res in query) {
Console.WriteLine(res.Name + " , " + res.Id);
}
}
 
//実際に条件を判定するメソッド。引数がnullでない時にチェックする。
private bool Filter(TestClass cls, string filterName, int? filterId) {
if (filterName != null && !cls.Name.Contains(filterName)){
return false;
}
if (filterId.HasValue && cls.Id != filterId.Value) {
return false;
}
return true;
}
}
 
public class TestClass {
public string Name { get; set; }
public int Id { get; set; }
}

ただ上記の Filter メソッドの場合、条件がANDだけになってしまいます。
OR条件にするは Filter メソッドを下記のようにします。

private bool Filter(TestClass cls, string filterName, int? filterId) {
if (filterName != null && cls.Name.Contains(filterName)) {
return true;
}
if (filterId.HasValue && cls.Id == filterId.Value) {
return true;
}
return false;
}

ただ複雑な条件は指定できないので、やはりラムダ式で式ツリーを生成する方法じゃないとダメでしょうかね。。。


ところで、LINQですが上記のようなクエリ構文とは別にメソッド構文というものも有ります。
両者のコードを下記に載せてみます。(文字数の関係でハイライトOFFです。)

//テスト用のデータ生成
List lst = new List();
lst.Add(new TestClass() { Name = "a", Id = 1 });
lst.Add(new TestClass() { Name = "b", Id = 2 });
lst.Add(new TestClass() { Name = "c", Id = 3 });
lst.Add(new TestClass() { Name = "d", Id = 5 });
lst.Add(new TestClass() { Name = "e", Id = 5 });
lst.Add(new TestClass() { Name = "f", Id = 4 });
lst.Add(new TestClass() { Name = "g", Id = 5 });

//検索条件の値。
int iFind=5;
string strFind = "a";

//LINQ クエリ構文での指定。Id が 5 またはNameが a のデータ
var query = from res in lst
where res.Id == iFind ||
res.Name.Equals("a")
select res;

//LINQ メソッド構文での指定。Id が 5 またはNameが a のデータ
var query2 = lst.Where(pram => pram.Id == iFind ||
pram.Name.Equals("a"))
.Select(pram => pram);


メソッド構文ではラムダ式を使っています。

ラムダ式もよくわかってませんが、C#2.0でデリゲートを簡単にかけるために匿名メソッドができました。その匿名メソッドをさらに簡潔に書けるようにしたもののようです。

=>演算子(goes to演算子)は「~に入力」と読むようですね。
左辺がデリゲートとして呼び出されるメソッドのパラメータが入るようです。(パラメータが複数場合は()でくくる必要がありますが、今回は一つなのでくくっていません)
右辺には式や文が入ります。ただLINQで使う場合は式が多いんじゃないかと思います。
ラムダ式の詳細は、@IT:C#ラムダ式 基礎文法最速マスタ@IT:第1回 ラムダ式 が分かりやすいです。

LINQのクエリ構文もメソッド構文もコンパイルすると全く同じコードになるようです。(@IT:第8回 LINQメソッド形式編が参考になります。MS的には、LINQ クエリ構文とメソッド構文 (C#)で「クエリ構文の方が単純で読みやすいため、クエリ構文を使用することをお勧めします。」とありました。)


ただ、オブジェクトLISTのWhereメソッドで検索する時、匿名メソッドだと無駄なコード吐き出すので、LINQのラムダかメソッド構文がいいということが、匿名メソッドとラムダ、そして LINQ の違い - vol 1,匿名メソッドとラムダ、そして LINQ の違い - vol 2で論じられてました。


言語の進歩に取り残されてる感が漂う3流PGでした。

追記:
動的に条件を設定する方法ですが、(.Net)LINQでラムダ式を動的に生成するに書いてみました。
[PR]
by jehoshaphat | 2011-06-06 22:52 | .Net開発 | Trackback | Comments(0)
(.Net)WindowsAPIのエラーコードからメッセージを取得する
.NET から WindowsAPI(Win32API) を呼び出した場合大抵の Win32API は戻り値としてエラーコードを返します。
このエラーコードはWin32 エラー コードと呼ばれてるようですが、System.ComponentModel.Win32Exception クラスの Message メソッドを使うとエラーコードのメッセージ文字列が取得できるようです。

ということで、参考先のコードそのままですがWin32エラーコードからエラーメッセージと16進数のエラーコードを返すメソッドです。(C#)

/// <summary>Win32エラーコードから、エラーを説明するメッセージと16進数表現を返します。</summary>
/// <param name="code">Win32エラーコード</param>
/// <returns>メッセージ+16進数</returns>
public static string FormatMessage(int code)
{
byte[] b = BitConverter.GetBytes(code);
Array.Reverse(b);//リトルエンディアンなので
string bs = "0x" + BitConverter.ToString(b, 0).Replace("-", "");
 
return string.Format("{0}({1})", new System.ComponentModel.Win32Exception(code).Message, bs);
}


参考:
C#メモ - shise.net
以下Win32のエラーコードを載せているサイトです。
Windowsシステムエラーコード一覧
Win32エラーコード一覧
Network Management Error Codes (Windows) ネットワーク系のみ。
[PR]
by jehoshaphat | 2011-06-06 22:36 | .Net開発 | Trackback | Comments(0)
(.Net)C#3.0からオブジェクトのコレクションの初期化が簡潔に書けるようになってた

C#3.0(.Net Framwork3.5)からオブジェクトの初期化が簡単に書けるようになってたようです。

以前(C#2.0)はこのように書いてました。

public class TestClass{
public string Name { get; set; }
public int Id { get; set; }
}
 
 
private void button1_Click(object sender, EventArgs e){
//テスト用のデータ生成
List<TestClass> lst = new List<TestClass>();
lst.Add(new TestClass() );
lst[0].Id = 2;
lst[0].Name = "a";
lst.Add(new TestClass() );
lst[1].Id = 1;
lst[1].Name = "b";
}

しかし、C#3.0からは下記のようにすることで、インスタンス作成時に1行でPublicメンバの初期化ができます。

private void button1_Click(object sender, EventArgs e){
//テスト用のデータ生成
List<TestClass> lst = new List<TestClass>();
lst.Add(new TestClass() { Name="a",Id = 2 });
lst.Add(new TestClass() { Name="b",Id = 1 });
}


ただこのようなインスタンス作成時の初期化はそれ用のコンストラクタを作成して使うべきだと思うんですがどうでしょう。。


また、インスタンス作成時だけでなくコレクションでも初期化が簡潔に書けるようになったようです。
上記で取り上げたオブジェクトイニシャライザとコレクション初期化子を組み合わせると下記のようになります。(C#)

private void button1_Click(object sender, EventArgs e){
List<TestClass> dataSource2 = new List<TestClass>() {
new TestClass() { Name="a",Id = 2 },
new TestClass() { Name="b",Id = 1 }
};
}


参考:
CSharp 3.0 - CSharp - Jankh メモ Wiki
[PR]
by jehoshaphat | 2011-06-06 22:32 | .Net開発 | Trackback | Comments(0)
Firefoxでキャッシュ先を変更する
自宅PCのメモリを増強し、RAMDiskを使うようになったので、RAMDisk上にFirefoxのキャッシュを置くことでレスポンスの向上を試みました。

Firefoxのキャッシュ置き場を変更するには、about:config からする方法と、user.js から設定する方法があるようですっが、手っ取り早い about:config を使ってみました。

アドレスバーに about:config と打ち込み、設定画面を出します。
ここで、browser.cache.disk.parent_directory という設定名を探し、もしなければ、右クリック→新規作成→文字列 で新しい設定を定義します。(設定名は browser.cache.disk.parent_directory )
値に、キャッシュしたいフォルダパスを指定します。

後は、Firefoxを再起動すれば設定が適用されます。

普段かなりタブを開いて使う方なので、タブの切換えの体感速度が速くなった気がします。

参考:
親バカITエンジニア日記 FirefoxのキャッシュをRAMディスクに置いてみた user.jsを使う方法も載っています。
[PR]
by jehoshaphat | 2011-06-06 22:26 | 豆知識 | Trackback | Comments(0)