[C#]从.NET中委托写法的演变谈开去(中):Lambda表达式及其优势

mikel阅读(543)

上一篇文章中我们简单探讨了.NET 1.x和.NET 2.0中委托表现形式的变化,以及.NET 2.0中匿名方法的优势、目的及注意事项。那么现在我们来谈一下.NET 3.5(C# 3.0)中,委托的表现形式又演变成了什么样子,还有什么特点和作用。

.NET 3.5中委托的写法(Lambda表达式)

  Lambda表达式在C#中的写法是“arg-list => expr-body”,“=>”符号左边为表达式的参数列表,右边则是表达式体(body)。参数列表可以包含0到多个参数,参数之间使用逗号分割。例如,以下便是一个使用Lambda表达式定义了委托的示例1

Func<int, int, int> max = (int a, int b) =>
{
if (a > b)
{
return a;
}
else
{
return b;
}
};

  与上文使用delegate定义匿名方法的作用相同,Lambda表达式的作用也是为了定义一个匿名方法。因此,下面使用delegate的代码和上面是等价的:

Func<int, int, int> max = delegate(int a, int b)
{
if (a > b)
{
return a;
}
else
{
return b;
}
};

  那么您可能就会问,这样看来Lambda表达式又有什么意义呢?Lambda表达式的意义便是它可以写的非常简单,例如之前的Lambda表达式可以简写成这样:

Func<int, int, int> max = (a, b) =>
{
if (a > b)
{
return a;
}
else
{
return b;
}
};

  由于我们已经注明max的类型是Func<int, int, int>,因此C#编译器可以明确地知道a和b都是int类型,于是我们就可以省下参数之前的类型信息。这个特性叫做“类型推演”,也就是指编译器可以自动知道某些成员的类型2。请不要轻易认为这个小小的改进意义不大,事实上,您会发现Lambda表达式的优势都是由这一点一滴的细节构成的。那么我们再来一次改变:

Func<int, int, int> max = (a, b) => a > b ? a : b;

  如果Lambda表达式的body是一个表达式(expression),而不是语句(statement)的话,那么它的body就可以省略大括号和return关键字。此外,如果Lambda表达式只包含一个参数的话,则参数列表的括号也可以省略,如下:

Func<int, bool> positive = a => a > 0;

  如今的写法是不是非常简单?那么我们来看看,如果是使用delegate关键字来创建的话会成为什么样子:

Func<int, bool> positive = delegate(int a)
{
return a > 0;
};

  您马上就可以意识到,这一行和多行的区别,这几个关键字和括号的省略,会使得编程世界一下子变得大为不同。

  当然,Lambda表达式也并不是可以完全替代delegate写法,例如带ref和out关键字的匿名方法,就必须使用.NET 2.0中的delegate才能构造出来了。

使用示例一

  Lambda表达式的增强在于“语义”二字。“语义”是指代码所表现出来的含义,说的更通俗一些,便是指一段代码给阅读者的“感觉”如何。为了说明这个例子,我们还是使用示例来说明问题。

  第一个例子是这样的:“请写一个方法,输入一个表示整型的字符串列表,并返回一个列表,包含其中偶数的平方,并且需要按照平方后的结果排序”。很简单,不是吗?相信您一定可以一蹴而就:

static List<int> GetSquaresOfPositive(List<string> strList)
{
List<int> intList = new List<int>();
foreach (var s in strList) intList.Add(Int32.Parse(s));
List<int> evenList = new List<int>();
foreach (int i in intList)
{
if (i % 2 == 0) evenList.Add(i);
}
List<int> squareList = new List<int>();
foreach (int i in evenList) squareList.Add(i * i);
squareList.Sort();
return squareList;
}

  我想问一下,这段代码给您的感觉是什么?它给我的感觉是:做了很多事情。有哪些呢?

  1. 新建一个整数列表intList,把参数strList中所有元素转化为整型保存起来。
  2. 新建一个整数列表evenList,把intList中的偶数保存起来。
  3. 新建一个整数列表squareList,把evenList中所有数字的平方保存起来。
  4. 将squareList排序。
  5. 返回squareList。

  您可能会问:“当然如此,还能怎么样?”。事实上,如果使用了Lambda表达式,代码就简单多了:

static List<int> GetSquaresOfPositiveByLambda(List<string> strList)
{
return strList
.Select(s => Int32.Parse(s)) // 转成整数
.Where(i => i % 2 == 0) // 找出所有偶数
.Select(i => i * i) // 算出每个数的平方
.OrderBy(i => i) // 按照元素自身排序
.ToList(); // 构造一个List
}

  配合.NET 3.5中定义的扩展方法,这段代码可谓“一气呵成”(在实际编码过程中,老赵更倾向于把这种简短的“递进式”代码写作一行)。那么这行代码的“语义”又有 什么变化呢?在这里,“语义”的变化在于代码的关注点从“怎么做”变成了“做什么”。这就是Lambda表达式的优势。

  在第一个方法中,我们构造了多个容器,然后做一些转化,过滤,并且向容器填充内容。其实这些都是“怎么做”,也就是所谓的“how (to do)”。但是这些代码并不能直接表示我们想要做的事情,我们想要做的事情其实是“得到XXX”,“筛选出YYY”,而不是“创建容器”,“添加元素”等 操作。

  在使用Lambda表达式的实现中,代码变得“声明式(declarative)”了许多。所谓“声明式”,便是“声称代码在做什么”,而不像 “命令式(imperative)”的代码在“操作代码怎么做”。换句话说,“声明式”关注的是“做什么”,是指“what (to do)”。上面这段声明式的代码,其语义则变成了:

  1. 把字符串转化为整数
  2. 筛选出所有偶数
  3. 把每个偶数平方一下
  4. 按照平方结果自身排序 
  5. 生成一个列表

  至于其中具体是怎么实现的,有没有构造新的容器,又是怎么向容器里添加元素的……这些细节,使用Lambda表达式的代码一概不会关心——这又不是我们想要做的事情,为什么要关心它呢?

  虽然扩展方法功不可没,但我认为,Lambda表达式在这里的重要程度尤胜前者,因为它负责了最关键的“语义”。试想,“i => i * i”给您的感觉是什么呢?是构造了一个委托吗(当然,您一定知道在这里其实构造了一个匿名方法)?至少对我来说,它的含义是“把i变成i * i”;同样,“i => i % 2 == 0”给我的感觉是“(筛选标准为)i模2等于零”,而不是“构造一个委托,XXX时返回true,否则返回false”;更有趣的是,OrderBy(i => i)给我的感觉是“把i按照i自身排序”,而不是“一个返回i自身的委托”。这一切,都是在“声明”这段代码在“做什么”,而不是“怎么做”。

  没错,“类型推演”,“省略括号”和“省略return关键字”可能的确都是些“细小”的功能,但也正是这些细微之处带来了编码方式上的关键性改变。

使用示例二

  使用Lambda表达式还可以节省许多代码(相信您从第一个示例中也可以看出来了)。不过我认为,最省代码的部分更应该可能是其“分组”和“字典转化”等功能。因此,我们来看第二个示例。

  这个示例可能更加贴近现实。不知您是否关注过某些书籍后面的“索引”,它其实就是“列出所有的关键字,根据其首字母进行分组,并且要求对每组内部的关键字进行排序”。简单说来,我们需要的其实是这么一个方法:

static Dictionary<char, List<string>> GetIndex(IEnumerable<string> keywords) { ... }

  想想看,您会怎么做?其实不难(作为示例,我们这里只关注小写英文,也不关心重复关键字这种特殊情况):

static Dictionary<char, List<string>> GetIndex(IEnumerable<string> keywords)
{
// 定义字典
var result = new Dictionary<char, List<string>>();
// 填充字典
foreach (var kw in keywords)
{
var firstChar = kw[0];
List<string> groupKeywords;
if (!result.TryGetValue(firstChar, out groupKeywords))
{
groupKeywords = new List<string>();
result.Add(firstChar, groupKeywords);
}
groupKeywords.Add(kw);
}
// 为每个分组排序
foreach (var groupKeywords in result.Values)
{
groupKeywords.Sort();
}
return result;
}

  那么如果利用Lambda表达式及.NET框架中定义的扩展方法,代码又会变成什么样呢?请看:

static Dictionary<char, List<string>> GetIndexByLambda(IEnumerable<string> keywords)
{
return keywords
.GroupBy(k => k[0]) // 按照首字母分组
.ToDictionary( // 构造字典
g => g.Key, // 以每组的Key作为键
g => g.OrderBy(k => k).ToList()); // 对每组排序并生成列表
}

  光从代码数量上来看,前者便是后者的好几倍。而有关“声明式”,“what”等可读性方面的优势就不再重复了,个人认为它比上一个例子给人的“震撼”有过之而无不及。

  试想,如果我们把GetIndexByLambda方法中的Lambda表达式改成.NET 2.0中delegate形式的写法:

static Dictionary<char, List<string>> GetIndexByDelegate(IEnumerable<string> keywords)
{
return keywords
.GroupBy(delegate(string k) { return k[0]; })
.ToDictionary(
delegate(IGrouping<char, string> g) { return g.Key; },
delegate(IGrouping<char, string> g)
{
return g.OrderBy(delegate(string s) { return s; }).ToList();
});
}

  您愿意编写这样的代码吗?

  因此,Lambda表达式在这里还是起着决定性的作用。事实上正是因为有了Lambda表达式,.NET中的一些函数式编程特性才被真正推广开 来。“语言特性”决定“编程方式”的确非常有道理。这一点上Java是一个很好的反例:从理论上说,Java也有“内联”的写法,但是C#的使用快感在 Java那边还只能是个梦。试想GetIndexByLambda在Java中会是什么情况3

public Dictionary<Char, List<String>> GetIndexInJava(Enumerable<String> keywords)
{
return keywords
.GroupBy(
new Func<String, Char> {
public Char execute(String s) { return s.charAt(0); }
})
.ToDictionary(
new Func<Grouping<Char, String>, Char> {
public Char execute(IGrouping<Char, String> g) { return g.getKey(); }
},
new Func<Grouping<Char, String>, List<string>> {
public List<String> execute(IGrouping<Char, String> g)
{
return g
.OrderBy(
new Func<String, String> {
public String execute(String s) { return s; }
})
.ToList();
}
});
}

  一股语法噪音的 气息扑面而来,让人无法抵挡。由于Java中的匿名类型语法(即上面这种内联写法)连类型信息(new Func<String, Char>{ … }这样的代码)都无法省去,因此给人非常繁琐的感觉。面对这样的代码,您可能会有和我一样的想法:“还不如最普通的写法啊”。没错,这种函数式编程的风 格,由于缺乏语言特性支持,实在不适合在Java语言中使用。事实上,这种内联写法很早就出现了(至少在02、03年我还在使用Java的时候就已经有 了),但是那么多年下来一点改进都没有。而Lambda表达式出现之后,社区中立即跟进了大量项目,如MoqFluent NHibernate等等,充分运用了C# 3.0的这一新特性。难道这还不够说明问题吗?

  对了,再次推荐一下Scala语言,它的代码可以写的和C#一样漂亮。我不是Java平台的粉丝,更是Java语言的忠实反对者,但是我对Java平台上的Scala语言和开源项目都抱有强烈的好感。

  既然谈到了函数式编程,那么就顺便再多说几句。其实这两个例子都有浓厚的函数式编程影子在里面,例如,对于函数试编程来说,Where常被叫做 filter,Select常被叫做map。而.NET 3.5中定义的另一些方法在函数式编程里都有体现(如Aggregate相当于fold)。如果您对这方面感兴趣,可以关注Matthew Poswysocki提出的Functional C#类库。

总结

  既可以提高可读性,又能够减少代码数量,我实在找不出任何理由拒绝Lambda表达式。

  哦,对了,您可能会提到“性能”,这的确也是一个重要的方面,不过关于这个话题我们下次再谈。受篇幅限制,原本计划的“上”“下”两篇这次又不得不拆开了。至于其他的内容,也等讨论完性能问题之后再说吧。

  当然,世界上没有东西是完美的,如果您觉得Lambda表达式在某些时候会给您带来“危害”,那么也不妨使用delegate代替Lambda 表达式。例如,为了代码清晰,在某些时候还是显式地指明参数类型比较好。不过对我而言,在任何情况下我都会使用Lambda表达式——最多使用“(int a, string b) =>”的形式咯,我想总比“delegate(int a, string b)”要统一、省事一些吧。

相关文章

 

注1:严格说来,这里的body是一个“语句(statement)”,而不是“表达式 (expression)”。因为一个委托其实是一个方法,因此使用Lambda来表示一个委托,其中必然要包含“语句”。不过在目前的C# 中,Lambda表达式还有一个作用是构造一颗“表达式树”,而目前的C#编译器只能构造“表达式树”而不是“语句树”。

注2:事实上,在.NET 2.0使用delegate关键字定义匿名方法时已经可以有些许“类型推演”的意味了——虽然还是必须写明参数的类型,但是我们已经可以省略委托的类型了,不是吗?

注3:除非我们补充Func、Enumerable,Dictionary,Grouping等类型及API,否则这段代码在Java中是无法编译通过的。事实上,这段Java代码是我在记事本中写出来的。不过这个形式完全正确。

[C#]从.NET中委托写法的演变谈开去(上):委托与匿名方法

mikel阅读(639)

  在《关于最近面试的一点感想》 一文中,Michael同学谈到他在面试时询问对方“delegate在.net framework1.1,2.0,3.5各可以怎么写”这个问题。于是乎,有朋友回复道“请问楼主,茴香豆的茴有几种写法”,“当代孔乙己”,独乐,众 乐。看了所有的评论,除了某些朋友认为“的确不该不知道这个问题”之外,似乎没有什么人在明确支持楼主。

  不过我支持,为什么?因为我也提过出这样的问题。

  这样,我们暂且不提应聘“高级开发人员”的人,在“自称熟悉各版本.NET框架”的前提下,是否应该知道这个答案。我们也暂且不提Michael同学提问的“目的”是什么。老赵就先单独针对这个问题进行解释,然后谈谈自己为什么会提出这个问题吧。

   可能有一件事情需要说在前面,那就是:委托本身其实从来没有改变过,改变的一直都是委托的“写法”。因此更确切地说,改变的只是“编译器”。而本文所有 内容都用C#来实现,其实谈得也都是C#编译器本身——但是其实VB.NET也有变化啊。再由于.NET版本和C#版本的关系也是非常密切的,因此全文就 使用.NET版本进行指代了。

.NET 1.x中委托的写法

  委托,如果不追究细节,从表面上来看我们可以将其通俗地理解为一个安全的“函数指针”。当然,这个函数指针其实也是一个对象,有自己的成员,也会封装了被调用方的上下文等等。至于委托的定义和使用方式,则是这样的:

public delegate int SomeDelegate(string arg1, bool arg2);
public static int SomeMethod(string arg1, bool arg2) { return 0; }
public class SomeClass
{
public int SomeMethod(string a1, bool a2) { return 0; }
public event SomeDelegate SomeEvent;
}
static void Main(string[] args)
{
SomeClass someClass = new SomeClass();
SomeDelegate someDelegate = new SomeDelegate(someClass.SomeMethod);
someClass.SomeEvent += new SomeDelegate(SomeMethod);
}

  可见,在.NET 1.x中需要使用new DelegateType(…)的方式来创建一个委托对象。不过,作为委托对象内部的方法它既可以是实例方法,也可以是静态方法。此外,方法只需要匹配委托类型的签名和返回值即可,方法参数的名称不会成为约束。

  嗯,就是这么简单。

.NET 2.0中委托的写法

  .NET中的委托引入了范型,且写法略有简化:

public delegate TResult MyFunc<T1, T2, TResult>(T1 a1, T2 a2);
public static int SomeMethod(string a1, bool a2) { return 0; }
static void Main(string[] args)
{
MyFunc<string, bool, int> myFunc = SomeMethod;
}

  在.NET 2.0中,new DelegateType已经可以省略,开发人员可以直接将方法赋值给一个委托对象的引用。当然,这个改进不值一提,.NET 2.0中委托写法的关键在于引入了“匿名方法”:

public static void TestRequest(string url)
{
WebRequest request = HttpWebRequest.Create(url);
request.BeginGetResponse(delegate(IAsyncResult ar)
{
using (WebResponse response = request.EndGetResponse(ar))
{
Console.WriteLine("{0}: {1}", url, response.ContentLength);
}
},
null);
}

  匿名方法,简单地说就是内联在方法内部的委托对象,它的关键便在于形成了一个闭包(委托执行时所需的上下文)。如上面的代码 中,BeginGetResponse的第一个参数(委托)可以直接使用TestRequest方法的参数url,以及方法内的“局部”变量 request。如果没有匿名函数这个特性的话,代码写起来就麻烦了,例如在.NET 1.x中您可能就必须这么写:

折叠
public static void TestRequest(string url)
{
WebRequest request = HttpWebRequest.Create(url);
object[] context = new object[] { url, request };
request.BeginGetResponse(TestAsyncCallback, context);
}
public static void TestAsyncCallback(IAsyncResult ar)
{
object[] context = (object[])ar.AsyncState;
string url = (string)context[0];
WebRequest request = (WebRequest)context[1];
using (WebResponse response = request.EndGetResponse(ar))
{
Console.WriteLine("{0}: {1}", url, response.ContentLength);
}
}

  此时,我们往往会发现,开发人员需要花费大量的精力,为一小部分代码维护一大段上下文。例如在这段代码中,我们会将url和request对象 塞入一个object数组中,在回调函数中再通过危险的Cast操作恢复数据。如果您希望“强类型”,那么只能为每个回调创建一个新的上下文对象,维护起 来可能更加麻烦——要知道,在并行编程,异步调用越来越重要的今天,如果没有匿名方法自动保留上下文的特性,开发人员会为这些“额外工作”疲于奔命的。

  可能您会说,匿名方法的可读性不佳,因为需要“内联”。一个方法中内联太多,维护成本就上去了,所以匿名方法并不推荐使用。我想说的是,您错了。如果为了可维护性,要将方法独立拆开,也可以利用匿名方法的优势:

public static void TestRequest(string url)
{
WebRequest request = HttpWebRequest.Create(url);
request.BeginGetResponse(delegate(IAsyncResult ar)
{
TestAsyncCallback(ar, request, url);
}, null);
}
public static void TestAsyncCallback(IAsyncResult ar, WebRequest request, string url)
{
using (WebResponse response = request.EndGetResponse(ar))
{
Console.WriteLine("{0}: {1}", url, response.ContentLength);
}
}

  如果借助.NET 3.5中的Lambda表达式,代码可以写的更简单易读:

public static void TestRequest(string url)
{
WebRequest request = HttpWebRequest.Create(url);
request.BeginGetResponse(ar => TestAsyncCallback(ar, request, url), null);
}

匿名方法的作用

  千万不要小看匿名方法的作用,有些时候您认为它的作用仅限于上文描述,只是因为没有在某些问题上踏前一步。例如,对于那些只需要“按需创建”,且要“线程安全”的对象,您会怎么做呢?没错,可以使用Double Check:

private object m_mutex = new object();
private bool m_initialized = false;
private BigInstance m_instance = null;
public BigInstance Instance
{
get
{
if (!this.m_initialized)
{
lock (this.m_mutex)
{
if (!this.m_initialized)
{
this.m_instance = new BigInstance();
this.m_initialized = true;
}
}
}
return this.m_instance;
}
}

  嗯,做的很漂亮!那么……这样的属性再来一个,再来三个,再来五个呢?可能有些朋友就会开始大段地Copy & Paste,于是错误便难免了。这里有一件真人真事,以前某位同学在一堆这样的代码中迷茫了,说为什么用了这种方法,还是初始化了多次对象了?检查了半天 没有看出问题来。最后发现,原因是访问了错误的initialized变量(例如,在某个应该访问artistInitialized的地方访问了 articleInitialized)。可惜,大段时间已经被浪费了——更糟的是,心情也随之变差了。

  其实,Copy & Paste很明显没有遵守DRY原则啊。为什么不把它们封装在一处呢?例如:

折叠
public class Lazy<T>
{
public Lazy(Func<T> func)
{
this.m_initialized = false;
this.m_func = func;
this.m_mutex = new object();
}
private Func<T> m_func;
private bool m_initialized;
private object m_mutex;
private T m_value;
public T Value
{
get
{
if (!this.m_initialized)
{
lock (this.m_mutex)
{
if (!this.m_initialized)
{
this.m_value = this.m_func();
this.m_func = null;
this.m_initialized = true;
}
}
}
return this.m_value;
}
}
}

  于是,之前的代码就可以简化成这样了:

private Lazy<BigInstance> m_lazyInstance =
new Lazy<BigInstance>(delegate { return new BigInstance(); });
public BigInstance Instance { get { return this.m_lazyInstance.Value; } }

  还是太丑,上Lambda表达式!

private Lazy<BigInstance> m_lazyInstance =
new Lazy<BigInstance>(() => new BigInstance());
public BigInstance Instance { get { return this.m_lazyInstance.Value; } }

  如果没有匿名方法,许多容易使用的编程模型和方式都难以开展。例如,我们就不会有CacheHelper,也不会有AsyncTaskDispatcher(上),也很难利用“延迟”所带来的便利,更难以出现微软并行扩展、CCR等优秀框架。可以这么说,如果您不善于使用委托,您如果不知道如何合适地使用匿名方法,您在不自知的情况下可能就已经编写了大量额外的代码了。

  老赵平时的工作之一,便是为项目提供各种扩展API,可以让程序员们更愉快地进行开发工作,得到更好的生产力,让代码变得更加美好。如今C#有了匿名方法、Lambda表达式、表达式树、扩展方法等优秀的语言特性,真让我有“如鱼得水”的感觉。因此,我对于Java这样不思进取的语言可以说深恶痛绝(Java朋友们赶快学习Scala吧)。在看阅读大量Java开源项目代码时,我常有这样的感觉:“如果是C#的话,利用匿名方法,这个类不就可以不写,那个类就可以省略……”。没错,为了保留回调函数的上下文而创建一些类,对于C#程序员来说,的确是一件有些不可思议的事情。

  至于Lambda表达式以及其他话题,我们下次再说吧。

匿名方法的缺点

  匿名方法的优势在于自动形成闭包,而它的缺点也是让程序员“不自觉”地创建了闭包,这会让某些对象的生命周期加长。例如在一开始的 TestRequest方法中,表面上看起来url是参数,request是局部变量,有些朋友可能会认为它们在方法退出后就已经准备回收了。不过因为形 成了闭包,url和request已经“升级”为一个对象的域变量,它的生命周期延长了,延长至回调函数执行完毕。因此,一不注意可能就会产生一些莫名其妙的情况

  其实,这些都是“延迟”所带来的陷阱,作为一个优秀的开发人员,除了知道某个东西的作用和优势,也要知道它的问题,不是吗?

总结

  您现在还觉得.NET中的“委托”是一个简单的,只适合“初学者”,一搜就都知道的概念吗?

  您可能会说“是”,不过对我来说这真不简单,我也是慢慢才意识到这些的。虽然关于委托的大量内容可以在互联网上搜索而来,但还是有太多东西是需要在大量编程实践中积累下来的——等一下,这不就是“高级开发人员”和“初学者”的主要区别之一吗?

  嘲笑孔乙己的朋友们,你们在一味鄙视“茴”的四种写法的同时,说不定也失去了一个了解中国传统文化的机会呢!

  剩下的下次再说吧,Lambda表达式还等着我们。

[MVC]ASP.NET MVC实用集锦(2)

mikel阅读(776)

继续和大家分享一些MVC使用技巧与实例。
首先想再补充一下集锦(1)中 的扩展htmlhelper问题,其实在C#3.0,就已经提供了扩展方法这个新特性,我们可以在程序中扩展一些基本类型,也可以扩展一些自定义类型。例 如:String,Datetime ,Enumerable等,具体步骤与扩展htmlhelper没什么区别(还是提醒一下别忘记引用命名空间)。
继续(1)中的顺序往下进行吧.
5.在view与Mvc view user control中使用强类型。

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
    Inherits="System.Web.Mvc.ViewPage<Account>" %>
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<List<Account>>" %>

在view 或 Mvc view user control中的代码如下

<% if (Model != null) { %>
      <div><%= Model.Name %></div>
      <div><%= Model.Age %></div>
<% } %>

在controller中需要将Model赋值

public ActionResult ShowAccount()
{
      var account = new Account { Name = "Name" , Age = 18 };
      return View(account);
}

顺便一提:
当我需要呈现View的时候我会选择 return View()
当我需要ajax异步请求到controller做一些事情时,处理结束后,我会通过 return Json() 返回操作是否成功或返回的数据。
当我需要调用其他action时我会使用 RedirectToAction 或 RedirectToRoute。
当我什么都不需要做的时候我会使用 EmptyResult()。例如数据导出操作等。
6.UpdateModel与TryUpdateModel
虽然在MVC 中可以直接在Controller的方法中定义操作对象,然后通过表单提交装入数据到对象,如下:

public ActionResult Register(Account account)
{ //TODO }

但 我会使用UpdateModel和TryUpdateModel去做这些事情,还是简单说一下这两个方法的功能。如果用户操作的view是完成注册用户功 能,此时表单数据已经填写完整,提交表单后转入上面的方法( Register() , 去掉参数),在方法中使用如下代码:

public ActionResult Register()
{
      var account = new Account();
      UpdateModel(account);
      //TODO: Register
      return ……
}

这样表单中的数据可以装入 account 对象,后来发现有时总是不成功,程序报错,遂发现TryUpdateModel(),虽然执行后也会有错误,但是部分数据已经正确填入程序也并不会报错, 然后将没有填入的数据(问题就发生在这些数据上)单独处理。不能装入的数据通常都是由规律的,例如ID,view中通过DropDownList选择的数 据等等,所以可以使用重载方法将可能会出错的数据排除掉, string[] excludeProperties 参数,这样TryUpdateModel就不会报错了,虽然已经找到装入对象时出错的规律,但还是想找到更准确的判断方法,不想这样偷懒下去。偶然间注意 到 ModelState.IsValid 这句经常出现的代码,所以就试了一下,在TryUpdateModel语句执行后,在快速监视中添加了ModelState,果然可以看到出错的字段(里 面有error属性)。后面的操作就是一样的了,排除这些字段,单独处理!

7.在Action中使用FormCollection,6中使用 string[] excludeProperties 参数排除掉的字段可以通过下面的方法赋值

public ActionResult Register(FormCollection form)
{
      var account = new Account();
      // city在表单中是下拉框选择的(DropDownList)
      if (TryUpdateModel(account , null ,  null , new string[] { "City" } ))   // UpdateModel也可以重载
      {
            account.City = form["City"];
      }
      return ……
}

[MVC]在 ASP.NET MVC 中使用 HTTPS (SSL/TLS)

mikel阅读(806)

某些安全性较高的网页,如网上支付或用户登陆页面,可能会使用到https(SSL/TLS)来提高安全性。本文介绍了如何在ASP.NET MVC中强制某action使用https和如何进行向https页面的跳转。
我们先实现强制一个action使用https。这里写了一个RequireHttpsAttribute,它的作用是将非https连接转换成https连接,这样所有使用了RequireHttps这个filter的controller都会强制使用https连接。
 

 1 using System.Web.Mvc;
 2 
 3 namespace Snowdream.Demo.RequireHttps
 4 {
 5     public class RequireHttpsAttribute:AuthorizeAttribute
 6     {
 7         /// <summary>
 8         /// 重写OnAuthorization方法
 9         /// </summary>
10         /// <param name="filterContext"></param>
11         public override void OnAuthorization(AuthorizationContext filterContext)
12         {
13             // 如果已经是https连接则不处理,否则重定向到https连接
14             if (!filterContext.HttpContext.Request.IsSecureConnection)
15             {
16                 // 获取当前请求的Path
17                 string path = filterContext.HttpContext.Request.Path;
18 
19                 // 从web.config中获取host,也可以直接从httpContext中获取
20                 string host = System.Configuration.ConfigurationManager.AppSettings["HostName"];
21 
22                 // 从web.config中获取https的端口
23                 string port = System.Configuration.ConfigurationManager.AppSettings["HttpsPort"];
24 
25                 // 如果端口号为空表示使用默认端口,否则将host写成host:port的形式
26                 if (port != null)
27                 {
28                     host = string.Format("{0}:{1}", host, port);
29                 }
30 
31                 // 重定向到https连接
32                 filterContext.HttpContext.Response.Redirect(string.Format("https://{0}{1}", host, path));
33             }
34         }
35     }
36 }
37 

由于https和https服务使用不同的端口号,而且https不能绑定主机头,只能通过不同端口的方式来区分各个站点,所以这里将host和port信息写到了web.config里,以方便配置。在web.config的appsettings节加入如下信息即可

1 <appSettings>
2     <add key="HostName" value="localhost"/>
3     <add key="httpsPort" value="443"/>
4 </appSettings>
5 

HttpsPort可以不写,将使用默认的443。
然后在要使用https连接的controller或action前加上[RequireHttps],如

1 [RequireHttps]
2 public ActionResult About()
3 {
4     return View();
5 }
6 

这样,当我们用http://localhost/Home/About访问该页面时会自动跳到https://localhost/Home/About。 但是这样还有一个问题,网页中的链接都是http的,当点击进入需要使用https连接的网页时都要进行一次Redirect。所以我们要将网页中的链接 也改成https。这步不难,只需要将view中所有链接到https页面的Html.Action()使用适当的重载方法来写即可。ASP.NET MVC 1.0 RTM中提供了2种重载可以将protocol设置为https。在新建ASP.NET MVC Web Application后默认生成的站点中,shared文件夹下有site.master文件中有个指向/Home/About的 ActionLink。原来是

1 Html.ActionLink("Home""Index""Home")

我们对其进行改写

1 Html.ActionLink("About""About""Home""https""localhost""",nullnull)

这样,生成出来的链接就是https的了,点击以后直接会使用https连接而无需再进行一次Redirect,之后新的要到https页面的链接也可仿照次写法。
这 里又要用到hostName信息,我们之前已经将它写在web.config里了,所以可以专门写一个方法来获取web.config中的这部分信息并拼 接成这里需要的hostName字符串,或者还可以对HtmlHelper写一个扩展方法专门用于处理https的链接,这些可以在实际使用时做适当的优 化。
示例代码下载

[JQuery]如何在多个页面使用同一个HTML片段

mikel阅读(757)

下载源代码
问题描述
有一个比较复杂的HTML片段(A),如果把这个HTML片段嵌入到其他页面中(B,C,D….)。
问题的关键是在HTML片段中有大量的JavaScript逻辑需要处理,比如说分页,点击事件响应等。
对于这个问题,我们用一个简单的例子来说明:
“页面上有一个按钮,点击此按钮引入一个HTML片段,此HTML片段中有分页按钮。”
1. 使用IFrame
主页面,点击一个按钮向页面引入一个IFrame:

    <script type="text/javascript">
$(function() {
$("#clickToInsert").click(function() {
$("#placeholder").html('<iframe src="iframe.htm"></iframe>');
});
});
</script>
<input type="button" id="clickToInsert" value="Insert HTML" />
<div id="placeholder">
</div>

IFrame页面,模拟分页的情况:

    <script type="text/javascript">
$(function() {
var parent = $("#complex_page_segment");
$(".previous", parent).click(function() {
$(".content", parent).html("Previous Page Content");
});
$(".next", parent).click(function() {
$(".content", parent).html("Next Page Content");
});
});
</script>
<div id="complex_page_segment">
<input type="button" value="Previous Page" class="previous" />
<input type="button" value="Next Page" class="next" />
<div class="content">
Page Content</div>
</div>

2. AJAX返回页面片段,并注册事件
注:我们通过textarea来模拟返回的HTML片段。

    <script type="text/javascript">
$(function() {
$("#clickToInsert").click(function() {
$("#placeholder").html($("#clone").val());
var parent = $("#complex_page_segment");
$(".previous", parent).click(function() {
$(".content", parent).html("Previous Page Content");
});
$(".next", parent).click(function() {
$(".content", parent).html("Next Page Content");
});
});
});
</script>
<input type="button" id="clickToInsert" value="Insert HTML" />
<div id="placeholder">
</div>
<textarea id="clone" style="display: none;">
<div id="complex_page_segment">
<input type="button" value="Previous Page" class="previous" />
<input type="button" value="Next Page" class="next" />
<div class="content">Page Content</div>
</div>
</textarea>

由于我们需要在多个页面引用同一个HTML片段,这种方法导致大量事情处理被重复性的拷贝粘贴,明显我们需要将公共的方法提取出来。
3. AJAX返回页面片段,并调用页面片段中的函数注册事件

    <script type="text/javascript">
$(function() {
$("#clickToInsert").click(function() {
$("#placeholder").html($("#clone").val());
init_complex_page_segment();
});
});
</script>
<input type="button" id="clickToInsert" value="Insert HTML" />
<div id="placeholder">
</div>
<textarea id="clone" style="display: none;">
<script type="text/javascript">
function init_complex_page_segment() {
var parent = $("#complex_page_segment");
$(".previous", parent).click(function() {
$(".content", parent).html("Previous Page Content");
});
$(".next", parent).click(function() {
$(".content", parent).html("Next Page Content");
});
}
</script>
<div id="complex_page_segment">
<input type="button" value="Previous Page" class="previous" />
<input type="button" value="Next Page" class="next" />
<div class="content">Page Content</div>
</div>
</textarea>

其实我们可以更进一步,完全没必要手工调用这个函数,而是可以在返回的HTML片段中让其自动执行。
4. AJAX返回页面片段,其事件自动注册

    <script type="text/javascript">
$(function() {
$("#clickToInsert").click(function() {
$("#placeholder").html($("#clone").val());
});
});
</script>
<input type="button" id="clickToInsert" value="Insert HTML" />
<div id="placeholder">
</div>
<textarea id="clone" style="display: none;">
<script type="text/javascript">
$(function() {
var parent = $("#complex_page_segment");
$(".previous", parent).click(function() {
$(".content", parent).html("Previous Page Content");
});
$(".next", parent).click(function() {
$(".content", parent).html("Next Page Content");
});
});
</script>
<div id="complex_page_segment">
<input type="button" value="Previous Page" class="previous" />
<input type="button" value="Next Page" class="next" />
<div class="content">Page Content</div>
</div>
</textarea>

最后一种方法和第一种IFrame的方式是我们所推荐的。

[JQuery]遍历声明的Json数组

mikel阅读(799)

以前基本上都是在用AJAX从服务器获得Json格式数据,然后直接调用JQuery的getJson中的Data然后
$.each(data,function(){
//具体代码
});
就行了,今天做模板,需要声明好json数组然后遍历显示,代码如下:
var json=[{"Identifier":"1","Name":"lele"},{"Identifier":"2","Name":"fifi"},{"Identifier":"3","Name":"mikel"}];
$.each(data,function(){
alert(this.Name);
});
OR
$(json).each(function(){
alert(this.Name);
});

[Hook]Windows Hook 学习笔记(1)——基本概念

mikel阅读(693)

基本概念
      钩子(Hook),是Windows消息处理机制的一个平台,应用程序可以在上面设置子程以监视指定 窗口的某种消息,而且所监视的窗口可以是其他进程所创建的。当消息到达后,在目标窗口处理函数之前处理它。钩子机制允许应用程序截获处理window消息 或特定事件。
      钩子实际上是一个处理消息的程序段,通过系统调用,把它挂入系统。每当特定的消息发出,在没有到达目 的窗口前,钩子程序就先捕获该消息,亦即钩子函数先得到控制权。这时钩子函数即可以加工处理(改变)该消息,也可以不作处理而继续传递该消息,还可以强制 结束消息的传递。
运行机制
1、钩子链表和钩子子程:
      每一个Hook都有一个与之相关联的指针列表,称之为钩子链表,由系统来维护。这个列表的指针指向指 定的,应用程序定义的,被Hook子程调用的回调函数,也就是该钩子的各个处理子程。当与指定的Hook类型关联的消息发生时,系统就把这个消息传递到 Hook子程。一些Hook子程可以只监视消息,或者修改消息,或者停止消息的前进,避免这些消息传递到下一个Hook子程或者目的窗口。最近安装的钩子 放在链的开始,而最早安装的钩子放在最后,也就是后加入的先获得控制权。
      Windows 并不要求钩子子程的卸载顺序一定得和安装顺序相反。每当有一个钩子被卸载,Windows 便释放其占用的内存,并更新整个Hook链表。如果程序安装了钩子,但是在尚未卸载钩子之前就结束了,那么系统会自动为它做卸载钩子的操作。
      钩子子程是一个应用程序定义的回调函数(CALLBACK Function),不能定义成某个类的成员函数,只能定义为普通的C函数。用以监视系统或某一特定类型的事件,这些事件可以是与某一特定线程关联的,也可以是系统中所有线程的事件。
   钩子子程必须按照以下的语法:
LRESULT CALLBACK HookProc
(
int nCode,
WPARAM wParam,
LPARAM lParam
);
  • HookProc是应用程序定义的名字。
  • nCode参数是Hook代码,Hook子程使用这个参数来确定任务。这个参数的值依赖于Hook类型,每一种Hook都有自己的Hook代码特征字符集。
  • wParam和lParam参数的值依赖于Hook代码,但是它们的典型值是包含了关于发送或者接收消息的信息。
2、钩子的安装与释放:
      使用API函数SetWindowsHookEx()把一个应用程序定义的钩子子程安装到钩子链表 中。SetWindowsHookEx函数总是在Hook链的开头安装Hook子程。当指定类型的Hook监视的事件发生时,系统就调用与这个Hook关 联的Hook链的开头的Hook子程。每一个Hook链中的Hook子程都决定是否把这个事件传递到下一个Hook子程。Hook子程传递事件到下一个 Hook子程需要调用CallNextHookEx函数。
HHOOK SetWindowsHookEx(
     int idHook,      // 钩子的类型,即它处理的消息类型
     HOOKPROC lpfn,   // 钩子子程的地址指针。如果dwThreadId参数为0
// 或是一个由别的进程创建的线程的标识,
// lpfn必须指向DLL中的钩子子程。
// 除此以外,lpfn可以指向当前进程的一段钩子子程代码。
// 钩子函数的入口地址,当钩子钩到任何消息后便调用这个函数。
     HINSTANCE hMod,  // 应用程序实例的句柄。标识包含lpfn所指的子程的DLL。
// 如果dwThreadId 标识当前进程创建的一个线程,
// 而且子程代码位于当前进程,hMod必须为NULL。
// 可以很简单的设定其为本应用程序的实例句柄。
     DWORD dwThreadId // 与安装的钩子子程相关联的线程的标识符。
// 如果为0,钩子子程与所有的线程关联,即为全局钩子。
                 );   
      函数成功则返回钩子子程的句柄,失败返回NULL。
  以上所说的钩子子程与线程相关联是指在一钩子链表中发给该线程的消息同时发送给钩子子程,且被钩子子程先处理。
在钩子子程中调用得到控制权的钩子函数在完成对消息的处理后,如果想要该消息继续传递,那么它必须调用另外一个 SDK中的API函数CallNextHookEx来传递它,以执行钩子链表所指的下一个钩子子程。这个函数成功时返回钩子链中下一个钩子过程的返回值, 返回值的类型依赖于钩子的类型。这个函数的原型如下:
LRESULT CallNextHookEx
(
HHOOK hhk;
int nCode;
WPARAM wParam;
LPARAM lParam;
); 
  • hhk为当前钩子的句柄,由SetWindowsHookEx()函数返回。
  • NCode为传给钩子过程的事件代码。
  • wParam和lParam 分别是传给钩子子程的wParam值,其具体含义与钩子类型有关。
      钩子函数也可以通过直接返回TRUE来丢弃该消息,并阻止该消息的传递。否则的话,其他安装了钩子的应用程序将不会接收到钩子的通知而且还有可能产生不正确的结果。
      钩子在使用完之后需要用UnHookWindowsHookEx()卸载,否则会造成麻烦。释放钩子比较简单,UnHookWindowsHookEx()只有一个参数。函数原型如下:
UnHookWindowsHookEx
(
HHOOK hhk;
);
      函数成功返回TRUE,否则返回FALSE。
 
3、一些运行机制:
      在Win16环境中,DLL的全局数据对每个载入它的进程来说都是相同的;而在Win32环境中,情 况却发生了变化,DLL函数中的代码所创建的任何对象(包括变量)都归调用它的线程或进程所有。当进程在载入DLL时,操作系统自动把DLL地址映射到该 进程的私有空间,也就是进程的虚拟地址空间,而且也复制该DLL的全局数据的一份拷贝到该进程空间。也就是说每个进程所拥有的相同的DLL的全局数据,它 们的名称相同,但其值却并不一定是相同的,而且是互不干涉的。
      因此,在Win32环境下要想在多个进程中共享数据,就必须进行必要的设置。在访问同一个Dll的各 进程之间共享存储器是通过存储器映射文件技术实现的。也可以把这些需要共享的数据分离出来,放置在一个独立的数据段里,并把该段的属性设置为共享。必须给 这些变量赋初值,否则编译器会把没有赋初始值的变量放在一个叫未被初始化的数据段中。
      #pragma data_seg预处理指令用于设置共享数据段。例如:
#pragma data_seg("SharedDataName")
HHOOK hHook=NULL;
#pragma data_seg()
 
      在#pragma data_seg("SharedDataName")和#pragma data_seg()之间的所有变量将被访问该Dll的所有进程看到和共享。再加上一条指令#pragma comment(linker,"/section:.SharedDataName,rws"),那么这个数据节中的数据可以在所有DLL的实例之间共 享。所有对这些数据的操作都针对同一个实例的,而不是在每个进程的地址空间中都有一份。
      当进程隐式或显式调用一个动态库里的函数时,系统都要把这个动态库映射到这个进程的虚拟地址空间里(以下简称"地址空间")。这使得DLL成为进程的一部分,以这个进程的身份执行,使用这个进程的堆栈。
 
4、系统钩子与线程钩子:
      SetWindowsHookEx()函数的最后一个参数决定了此钩子是系统钩子还是线程钩子。
      线程勾子用于监视指定线程的事件消息。线程勾子一般在当前线程或者当前线程派生的线程内。
      系统勾子监视系统中的所有线程的事件消息。因为系统勾子会影响系统中所有的应用程序,所以勾子函数必须放在独立的动态链接库(DLL) 中。系统自动将包含"钩子回调函数"的DLL映射到受钩子函数影响的所有进程的地址空间中,即将这个DLL注入了那些进程。
几点说明:
(1)如果对于同一事件(如鼠标消息)既安装了线程勾子又安装了系统勾子,那么系统会自动先调用线程勾子,然后调用系统勾子。
(2)对同一事件消息可安装多个勾子处理过程,这些勾子处理过程形成了勾子链。当前勾子处理结束后应把勾子信息传递给下一个勾子函数。
(3)勾子特别是系统勾子会消耗消息处理时间,降低系统性能。只有在必要的时候才安装勾子,在使用完毕后要及时卸载。
 
钩子类型
      每一种类型的Hook可以使应用程序能够监视不同类型的系统消息处理机制。下面描述所有可以利用的Hook类型。
1、WH_CALLWNDPROC和WH_CALLWNDPROCRET Hooks
WH_CALLWNDPROC和WH_CALLWNDPROCRET Hooks使你可以监视发送到窗口过程的消息。系统在消息发送到接收窗口过程之前调用WH_CALLWNDPROC Hook子程,并且在窗口过程处理完消息之后调用WH_CALLWNDPROCRET Hook子程。
WH_CALLWNDPROCRET Hook传递指针到CWPRETSTRUCT结构,再传递到Hook子程。
CWPRETSTRUCT结构包含了来自处理消息的窗口过程的返回值,同样也包括了与这个消息关联的消息参数。
2、WH_CBT Hook
      在以下事件之前,系统都会调用WH_CBT Hook子程,这些事件包括:
      1. 激活,建立,销毁,最小化,最大化,移动,改变尺寸等窗口事件;
      2. 完成系统指令;
      3. 来自系统消息队列中的移动鼠标,键盘事件;
      4. 设置输入焦点事件;
      5. 同步系统消息队列事件。
      Hook子程的返回值确定系统是否允许或者防止这些操作中的一个。
3、WH_Debug Hook
      在系统调用系统中与其他Hook关联的Hook子程之前,系统会调用WH_Debug Hook子程。你可以使用这个Hook来决定是否允许系统调用与其他Hook关联的Hook子程。
4、WH_FOREGROUNDIDLE Hook
      当应用程序的前台线程处于空闲状态时,可以使用WH_FOREGROUNDIDLE Hook执行低优先级的任务。当应用程序的前台线程大概要变成空闲状态时,系统就会调用WH_FOREGROUNDIDLE Hook子程。
5、WH_GETMESSAGE Hook
      应用程序使用WH_GETMESSAGE Hook来监视从GetMessage or PeekMessage函数返回的消息。你可以使用WH_GETMESSAGE Hook去监视鼠标和键盘输入,以及其他发送到消息队列中的消息。
6、WH_JOURNALPLAYBACK Hook
      WH_JOURNALPLAYBACK Hook使应用程序可以插入消息到系统消息队列。可以使用这个Hook回放通过使用WH_JOURNALRECORD Hook记录下来的连续的鼠标和键盘事件。只要WH_JOURNALPLAYBACK Hook已经安装,正常的鼠标和键盘事件就是无效的。
      WH_JOURNALPLAYBACK Hook是全局Hook,它不能象线程特定Hook一样使用。
      WH_JOURNALPLAYBACK Hook返回超时值,这个值告诉系统在处理来自回放Hook当前消息之前需要等待多长时间(毫秒)。这就使Hook可以控制实时事件的回放。
      WH_JOURNALPLAYBACK是system-wide local hooks,它們不會被注射到任何行程位址空間。
7、WH_JOURNALRECORD Hook
      WH_JOURNALRECORD Hook用来监视和记录输入事件。典型的,可以使用这个Hook记录连续的鼠标和键盘事件,然后通过使用WH_JOURNALPLAYBACK Hook来回放。
      WH_JOURNALRECORD Hook是全局Hook,它不能象线程特定Hook一样使用。
      WH_JOURNALRECORD是system-wide local hooks,它們不會被注射到任何行程位址空間。
8、WH_KEYBOARD Hook
      在应用程序中,WH_KEYBOARD Hook用来监视WM_KEYDOWN and WM_KEYUP消息,这些消息通过GetMessage or PeekMessage function返回。可以使用这个Hook来监视输入到消息队列中的键盘消息。
9、WH_KEYBOARD_LL Hook
      WH_KEYBOARD_LL Hook监视输入到线程消息队列中的键盘消息。
10、WH_MOUSE Hook
      WH_MOUSE Hook监视从GetMessage 或者 PeekMessage 函数返回的鼠标消息。使用这个Hook监视输入到消息队列中的鼠标消息。
11、WH_MOUSE_LL Hook
      WH_MOUSE_LL Hook监视输入到线程消息队列中的鼠标消息。
12、WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks
      WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks使我们可以监视菜单,滚动条,消息框,对话框消息并且发现用户使用ALT+TAB or ALT+ESC 组合键切换窗口。WH_MSGFILTER Hook只能监视传递到菜单,滚动条,消息框的消息,以及传递到通过安装了Hook子程的应用程序建立的对话框的消息。WH_SYSMSGFILTER Hook监视所有应用程序消息。
      WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks使我们可以在模式循环期间过滤消息,这等价于在主消息循环中过滤消息。
      通过调用CallMsgFilter function可以直接的调用WH_MSGFILTER Hook。通过使用这个函数,应用程序能够在模式循环期间使用相同的代码去过滤消息,如同在主消息循环里一样。
13、WH_SHELL Hook
      外壳应用程序可以使用WH_SHELL Hook去接收重要的通知。当外壳应用程序是激活的并且当顶层窗口建立或者销毁时,系统调用WH_SHELL Hook子程。
      WH_SHELL 共有5钟情況:
      1. 只要有个top-level、unowned 窗口被产生、起作用、或是被摧毁;
      2. 当Taskbar需要重画某个按钮;
      3. 当系统需要显示关于Taskbar的一个程序的最小化形式;
      4. 当目前的键盘布局状态改变;
      5. 当使用者按Ctrl+Esc去执行Task Manager(或相同级别的程序)。
      按照惯例,外壳应用程序都不接收WH_SHELL消息。所以,在应用程序能够接收WH_SHELL消息之前,应用程序必须调用SystemParametersInfo function注册它自己。
 

[Hook]Winform:关于钩子的基础知识

mikel阅读(870)

基本概念
钩子(Hook),是Windows消息处理机制的一个平台,应用程序可以在上面设置子程以监视指定窗口的某种消息,而且所监视的窗口可以是其他进程所创建的。当消息到达后,在目标窗口处理函数之前处理它。钩子机制允许应用程序截获处理window消息或特定事件。
钩子实际上是一个处理消息的程序段,通过系统调用,把它挂入系统。每当特定的消息发出,在没有到达目的窗口前,钩子程序就先捕获该消息,亦即钩子函数先得到控制权。这时钩子函数即可以加工处理(改变)该消息,也可以不作处理而继续传递该消息,还可以强制结束消息的传递。

 

http://blog.csdn.net/hejinjiang/archive/2008/03/19/2197066.aspx有对勾子作了非常详细的介绍

 

尽管在 .NET 框架中不支持全局挂钩

MSDN

您无法在 Microsoft .NET 框架中实现全局挂钩。若要安装全局挂钩,挂钩必须有一个本机动态链接库 (DLL) 导出以便将其本身插入到另一个需要调入一个有效而且一致的函数的进程中。这需要一个 DLL 导出,而 .NET 框架不支持这一点。托管代码没有让函数指针具有统一的值这一概念,因为这些函数是动态构建的代理。

 

但我们通过API函数的调用,还是可以照样实现十年前很热门的技术

 

勾子到底能干嘛呢?

提示: 如果要设置系统级钩子, 钩子函数必须在 DLL .


SetWindowsHookEx(

 idHook: Integer;   {钩子类型}

 lpfn: TFNHookProc; {函数指针}

 hmod: HINST;       {包含钩子函数的模块(EXEDLL)句柄; 一般是 HInstance; 如果是当前线程这里可以是 0}

 dwThreadId: DWORD {关联的线程; 可用 GetCurrentThreadId 获取当前线程; 0 表示是系统级钩子}

): HHOOK;            {返回钩子的句柄; 0 表示失败}

 

//钩子类型 idHook 选项:

WH_MSGFILTER       = -1; {线程级; 截获用户与控件交互的消息}

WH_JOURNALRECORD   = 0; {系统级; 记录所有消息队列从消息队列送出的输入消息, 在消息从队列中清除时发生; 可用于宏记录}

WH_JOURNALPLAYBACK = 1; {系统级; 回放由 WH_JOURNALRECORD 记录的消息, 也就是将这些消息重新送入消息队列}

WH_KEYBOARD        = 2; {系统级或线程级; 截获键盘消息}

WH_GETMESSAGE      = 3; {系统级或线程级; 截获从消息队列送出的消息}

WH_CALLWNDPROC     = 4; {系统级或线程级; 截获发送到目标窗口的消息, SendMessage 调用时发生}

WH_CBT             = 5; {系统级或线程级; 截获系统基本消息, 譬如: 窗口的创建、激活、关闭、最大最小化、移动等等}

WH_SYSMSGFILTER    = 6; {系统级; 截获系统范围内用户与控件交互的消息}

WH_MOUSE           = 7; {系统级或线程级; 截获鼠标消息}

WH_HARDWARE        = 8; {系统级或线程级; 截获非标准硬件(非鼠标、键盘)的消息}

WH_Debug           = 9; {系统级或线程级; 在其他钩子调用前调用, 用于调试钩子}

WH_SHELL           = 10; {系统级或线程级; 截获发向外壳应用程序的消息}

WH_FOREGROUNDIDLE = 11; {系统级或线程级; 在程序前台线程空闲时调用}

WH_CALLWNDPROCRET = 12; {系统级或线程级; 截获目标窗口处理完毕的消息, SendMessage 调用后发生}

 

从这个结构体与其枚举参数,这是仁者见仁的事情,我们同事一直都在做用钩子控制别人软件,如QQ的事情,嘿嘿,其实也即相当于木马了,比如某某监听健盘而获取你密码的软件。而我们经常用的ctrl+alt+z调出QQ,也可以通过做一个全局的钩子来应用(热健的下面再介绍)

 

这里介绍了一篇勾住MOUSE的方法

http://support.microsoft.com/kb/318804/

而这里也介绍了一篇控制KEYBOARD的方法

http://www.cnblogs.com/zagelover/articles/1137331.html

 

而其最主要的就是下面这么几个API函数,而委托作为函数指针也在这里有个比较完美的体现吧,其实就充当回调函数的指针。

 

 

 //Declare wrapper managed MouseHookStruct class.

        [StructLayout(LayoutKind.Sequential)]

        public class MouseHookStruct

        {

            public POINT pt;

            public int hwnd;

            public int wHitTestCode;

            public int dwExtraInfo;

        }

//声明键盘钩子的封送结构类型
        [StructLayout(LayoutKind.Sequential)]
        public class KeyboardHookStruct
        {
            public int vkCode; //表示一个在1254间的虚似键盘码
            public int scanCode; //表示硬件扫描码
            public int flags;
            public int time;
            public int dwExtraInfo;
        }
        //装置钩子的函数
        [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
        public static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hInstance, int threadId);

        //卸下钩子的函数
        [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
        public static extern bool UnhookWindowsHookEx(int idHook);

        //下一个钩挂的函数
        [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
        public static extern int CallNextHookEx(int idHook, int nCode, Int32 wParam, IntPtr lParam);

        [DllImport("user32")]
        public static extern int ToAscii(int uVirtKey, int uScanCode, byte[] lpbKeyState, byte[] lpwTransKey, int fuState);

注:

ToAscii Function


The ToAscii function translates the specified virtual-key code and keyboard state to the corresponding character or characters. The function translates the code using the input language and physical keyboard layout identified by the keyboard layout handle.

To specify a handle to the keyboard layout to use to translate the specified code, use the ToAsciiEx function.

Return Value

If the specified key is a dead key, the return value is negative. Otherwise, it is one of the following values.

Value

Meaning

0

The specified virtual key has no translation for the current state of the keyboard.

1

One character was copied to the buffer.

2

Two characters were copied to the buffer. This usually happens when a dead-key character (accent or diacritic) stored in the keyboard layout cannot be composed with the specified virtual key to form a single character.

通过这个函数,有时也可以实现某些热健功能,特别是返回值为1    

到这里也顺便复习下KEYPRESS

在控件有焦点的情况下按下键时发生

键事件按下列顺序发生:

KeyDown

KeyPress

KeyUp

非字符键不会引发 KeyPress 事件;但非字符键却可以引发 KeyDown KeyUp 事件。

使用 KeyChar 属性在运行时对键击进行取样,并且使用或修改公共键击的子集。

若要仅在窗体级别处理键盘事件而不允许其他控件接收键盘事件,请将窗体的 KeyPress 事件处理方法中的 KeyPressEventArgs.Handled 属性设置为 true

   说明  
   
  具有焦点的对象接收该事件。一个窗体仅在它没有可视和有效的控件或   KeyPreview   属性被设置为   True   时才能接收该事件。一个   KeyPress   事件可以引用任何可打印的键盘字符,一个来自标准字母表的字符或少数几个特殊字符之一的字符与   CTRL   键的组合,以及   ENTER   或   BACKSPACE   键。KeyPress   事件过程在截取   TextBox   或   ComboBox   控件所输入的击键时是非常有用的。它可立即测试击键的有效性或在字符输入时对其进行格式处理。改变   keyascii   参数的值会改变所显示的字符。  
   
  可使用下列表达式将   keyascii   参数转变为一个字符:  
   
  Chr(KeyAscii)  
   
  然后执行字符串操作,并将该字符反译成一个控件可通过该表达式解释的   ANSI   数字:  
   
  KeyAscii   =   Asc(char)  
   
  应当使用   KeyDown   和   KeyUP   事件过程来处理任何不被   KeyPress   识别的击键,诸如:功能键、编辑键、定位键以及任何这些键和键盘换档键的组合等。与   KeyDown   和   KeyUp   事件不同的是,KeyPress   不显示键盘的物理状态,而只是传递一个字符。  
   
  KeyPress   将每个字符的大、小写形式作为不同的键代码解释,即作为两种不同的字符。而   KeyDown   和   KeyUp   用两种参数解释每个字符的大写形式和小写形式:keycode   —   显示物理的键(将   A   和   a   作为同一个键返回)和   shift   —指示   shift   +   key   键的状态而且返回   A   或   a   其中之一。  
   
  如果   KeyPreview   属性被设置为   True,窗体将先于该窗体上的控件接收此事件。可用   KeyPreview   属性来创建全局键盘处理例程。  
   

  注意…CTRL+@   的键盘组合的   ANSI   编号是   0。因为   Visual   Basic   将一个零值的   keyascii   识别为一个长度为零的字符串   (""),在应用程序中应避免使用   CTRL+@   的组合。

 

 

在这里下面的委托将充当函数指针用

public delegate int HookProc(int nCode, Int32 wParam, IntPtr lParam);

[Hook]C#强化系列文章二:在C#中使用钩子

mikel阅读(870)

相信以前用过VB、Delphi,特别是VC的程序员应该对钩子程序都不陌生。在C#中我们同样可以使用钩子程序来实现特殊效果,比如当用户按下某个特殊键时提示,比如关闭应用程序前提示等。
当然使用方法相对VC来说要稍微复杂一点,有的地方还不太方便,下面的例子中实现两个基本功能:
1、按下Alt+F4时使窗口最小化
2、关闭应用程序前提示
不过目前只能捕获消息,不能屏蔽消息,我正在实验,也希望知道的高手能多多指教
一、加入winuser.h中的定义
因为钩子程序一般情况下都是在vc下使用的,在C#里面并没有对应的方法、结构等的定义,我们首先需要把winuser.h中的相关定义加入自己的类

钩子类型的枚举

具体的说明在msdn中都可以查到,主要的比如WH_KEYBOARD是监控按键事件,WH_CALLWNDPROC是在消息触发时执行

虚键值的定义

这个不用说明了,对应ALT、CTRL等键

消息结构体

这个是windows内部传递过来的消息的结构
二、加入自己定义的委托和事件参数

钩子委托

HokkProc是SetWindowsHookEx调用时的委托事件,HookEventHandler是自己的委托事件

钩子事件参数

是自己的委托事件中接受的事件参数
三、实现自己的钩子类
这一步是最重要的,要使用钩子,我们需要引用user32.dll中的相应方法:

        [DllImport("user32.dll")]
        
static extern IntPtr SetWindowsHookEx(HookType hook, HookProc callback, IntPtr hMod, uint dwThreadId);

        [DllImport(
"user32.dll")]
        
static extern bool UnhookWindowsHookEx(IntPtr hhk);

        [DllImport(
"user32.dll")]
        
static extern int CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

        [DllImport(
"user32.dll")]
        
static extern short GetKeyState(VirtualKeys nVirtKey);

SetWindowsHookEx是注册一个钩子程序,UnhookWindowsHookEx是释放钩子程序,CallNextHookEx调用钩子的后续事件处理,GetKeyState得到所按的虚键
然后就可以调用这些方法来实现钩子程序,比如注册一个钩子可以调用:

            m_hook = SetWindowsHookEx(m_hooktype, m_hookproc, IntPtr.Zero, (uint)AppDomain.GetCurrentThreadId());

其中m_hooktype就是HookType中定义的类型,m_hookproc就是实际的钩子处理程序:

m_hookproc = new HookProc(KeyHookProcedure);

最关键的就是KeyHookProcedure等钩子处理程序:

        protected int KeyHookProcedure(int code, IntPtr wParam, IntPtr lParam)
        
{
            
if (code != 0)
            
{
                
return CallNextHookEx(m_hook, code, wParam, lParam);
            }


            
if (HookInvoked != null)
            
{
                Keys key 
= (Keys)wParam.ToInt32();
                HookEventArgs eventArgs 
= new HookEventArgs();
                eventArgs.key 
= key;
                eventArgs.lParam 
= lParam;
                eventArgs.wParam 
= wParam;
                eventArgs.HookCode 
= code;
                eventArgs.bAltKey 
= GetKeyState(VirtualKeys.VK_MENU) <= 127;
                eventArgs.bCtrlKey 
= GetKeyState(VirtualKeys.VK_CONTROL) <= 127;
                HookInvoked(this, eventArgs);
            }


            
return CallNextHookEx(m_hook, code, wParam, lParam);
        }

在这个事件中可以取得消息的参数,特别是按键的值,然后通过HookInvoked委托调用事件实际的处理程序
四、在应用程序中调用钩子类
我们可以在自己的form中声明两个钩子对象

        private MyHook callProcHook = new MyHook(HookType.WH_CALLWNDPROC);
        
private MyHook keyHook = new MyHook(HookType.WH_KEYBOARD);

然后在初始化时注册钩子:

        private void Form1_Load(object sender, EventArgs e)
        
{
            keyHook.HookInvoked 
+= new HookEventHandler(keyHook_HookInvoked);
            keyHook.Install();

            callProcHook.HookInvoked 
+= new HookEventHandler(callProcHook_HookInvoked);
            callProcHook.Install();
        }

然后就是实际的钩子事件:

        private void keyHook_HookInvoked(object sender, HookEventArgs e)
        
{
            
if (e.key == Keys.F4 && e.bAltKey) //Alt + F4
            {
                
this.WindowState = FormWindowState.Minimized;
            }

        }


        
private void callProcHook_HookInvoked(object sender, HookEventArgs e)
        
{
            
unsafe
            
{
                CWPSTRUCT
* message = (CWPSTRUCT*)e.lParam;
                
if (message != null)
                
{
                    
if (message->message == WM_CLOSE)
                    
{
                        (sender 
as MyHook).CallNextProc = false;
                        MessageBox.Show(
"程序即将关闭!");
                    }

                }

            }

        }

这样我们就可以通过钩子实现一些相对底层的应用。
代码说的有点乱,我就把最主要的代码直接列在下面供大家参考:

例子代码

以上的钩子只对当前应用程序起作用,如果想控制其他的所有程序,需要使用全局钩子。原则上全局钩子在C#中是不支持的,在http://www.codeproject.com/csharp/globalhook.asp 中的代码可以参照来实现全局钩子

[Hook]API 通过HOOK OpenProcess() 实现进程防杀

mikel阅读(1611)

在WINDOWS操作系统下,当我们无法结束或者不知道怎样结束一个程序的时候,或者是懒得去找“退出”按钮的时候,通常会按 “CTRL+ALT+DEL”呼出任务管理器,找到想结束的程序,点一下“结束任务”就了事了,呵呵,虽然有点粗鲁,但大多数情况下都很有效,不是吗?
                 
    设想一下,如果有这么一种软件,它所要做的工作就是对某个使用者在某台电脑上的活动 作一定的限制,而又不能被使用者通过“结束任务”这种方式轻易地解除限制,那该怎么做?无非有这么三种方法:1.屏蔽“CTRL+ALT+DEL”这个热 键的组合;2.让程序不出现在任务管理器的列表之中;3.让任务管理器无法杀掉这个任务。对于第一种方法,这样未免也太残酷了,用惯了“结束任务”这种方 法的人会很不习惯的;对于第二种方法,在WINDOWS 9X下可以很轻易地使用注册服务进程的方法实现,但是对于WINDOWS  NT架构的操作系统没有这个方法了,进程很难藏身,虽然仍然可以实现隐藏,但 实现机制较为复杂;对于第三种方法,实现起来比较简单,我的作品:IPGate网址过滤器 就是采用的这种方式防杀的,接下来我就来介绍这种方法。
    任务管理器的“结束任务”实际上就是强制终止进程,它所使用的杀手锏是一个叫做TerminateProcess()的Win32 API函数,我们来看看它的定义:
            BOOL TerminateProcess(
              HANDLE      hProcess; // 将被结束进程的句柄
              UINT        uExitCode; // 指定进程的退出码
            );
    看 到这里,是不是觉得不必往下看都知道接下来要做什么:Hook TerminateProcess()函数,每次TerminateProcess()被调用的时候先判断企图结束的进程是否是我的进程,如果是的话就简 单地返回一个错误码就可以了。真的是这么简单吗?先提出一个问题,如何根据hProcess判断它是否是我的进程的句柄?答案是:在我的进程当中先获得我 的进程的句柄,然后通过进程间通讯机制传递给钩子函数,与hProcess进行比较不就行了?错!因为句柄是一个进程相关的值,不同进程中得到的我的进程 的句柄的值在进程间进行比较是无意义的。
    怎么办?我们来考察一下我的hProcess它是如何得到的。一个进程只有它的进程 ID是独一无二的,操作系统通过进程ID来标识一个进程,当某个程序要对这个进程进行访问的话,它首先得用OpenProcess这个函数并传入要访问的 进程ID来获得进程的句柄,来看看它的参数:
            HANDLE OpenProcess(
            DWORD      dwDesiredAccess, // 希望获得的访问权限
            BOOL       bInheritHandle, // 指明是否希望所获得的句柄可以继承
            DWORD      dwProcessId // 要访问的进程ID
            );
     脉 络渐渐显现:在调用TerminateProcess()之前,必先调用OpenProcess(),而OpenProcess()的参数表中的 dwProcessId是在系统范围内唯一确定的。得出结论:要Hook的函数不是TerminateProcess()而是 OpenProcess(),在每次调用OpenProcess()的时候,我们先检查dwProcessId是否为我的进程的ID(利用进程间通讯机 制),如果是的话就简单地返回一个错误码就可以了,任务管理器拿不到我的进程的句柄,它如何结束我的进程呢?
    至此,疑团全部 揭开了。由Hook TerminateProcess()到Hook OpenProcess()的这个过程,体现了一个逆向思维的思想。其实我当初钻进了TerminateProcess()的死胡同里半天出也不来,但最 终还是蹦出了灵感的火花,注意力转移到了OpenProcess()上面,实现了进程防杀。喜悦之余,将这心得体会拿出来与大家分享。