‘ .NET开发 ’ category archive

Linq to SQL 的更新冲突与管理

244 views 七月 03, 08 by Timothy

前段时间工作中的一个新需求,有机会用到了Linq to SQL。使用后的第一感觉,就是方便很多,也为整个项目节约了一大把的开发时间,甚至代码量也少了很多。不过在程序的实际运行中,始终会遇到一些莫名其妙的异常,最令人不解的,就是“System.Data.Linq.ChangeConflictException: Row not found or changed.” 。当初凭自己和同事的判断,可能是数据库的数据异常所导致,后来发觉这个异常出现得越来越频繁,于是上MSDN查了查,原来是Linq中一个常见的问题:更新冲突。
这个词说起来比较玄乎,其实再平常不过了。下面可以通过一个简单的例子,来重现这个异常。
建立一个普通的测试表:LinqTest(如图)

在测试表中,插入一条测试数据(如图)

测试代码如下:

[code]
namespace LinqTest
{
class Program
{
static void Main(string[] args)
{

TestDataContext db = new TestDataContext();

db.Log = Console.Out;
var result = from p in db.LinqTests
where p.ID == 1
select p;

var info = result.FirstOrDefault();

if(info != null) //插入断点
{
info.Age = 25;
db.SubmitChanges();
}

Console.ReadLine();

}

}

}
[/code]

在测试代码中,将DataContext的日志定向到Console的输出部分,这样方便我们观察Linq实际执行的SQL语句是什么。重现的时候,我们需要在注释的地方,插入断点进行测试。对于示例中的代码,在正常情况下,是不会有错误的。执行过后,我们可以在Console的输出中,看到实际执行的SQL语句(如图)

再进行第二次调试,首先,恢复Age的数据到以前的样子。下面我们运行到断点处,然后偷偷去SQL Server Management Studio中,手动修改数据,将原始数据中的Age,由24,改为22。然后回到VS2008的IDE,按F5继续运行程序,这个时候,你会发现异常出现了(如图)

再回到Console的输出,查看,执行的SQL语句和刚才的一样。这就是问题的所在,在正常运行状态下,Linq在运行时,会把数据库的数据缓存到实体对象中,这是一种理想化的情况,并且在更新时,Linq会默认把除更新字段外的所有字段,作为Update语句中的Where条件。但是,如果此时有另外的程序,在访问数据库,并修改数据库数据的时候,比如刚才把Age改为22。此时Linq缓存起来的数据和实际数据库中的数据产生了不一致的情况。Linq此时仍然把被修改过的字段,作为Update的Where条件,但是数据库中Age早就被我们改过了,不再是25,Where条件始终匹配不到原有的数据。这时,就会抛出所谓的:“System.Data.Linq.ChangeConflictException: Row not found or changed.”异常。

产生此异常,主要是Linq缓存数据和实际数据库数据不一致的情况造成。解决次问题的情况,主要有几种:

1.比较简单的方法,不使用Linq提供的SubmitChanges()方式提交更改,而直接执行SQL语句,例如:
db.ExecuteCommand(“Update [dbo].[LinqTest] SET Age=25 Where ID = @p0″, 1);
这样虽然比较方便,但是感觉又回到了直接写SQL的时代,毕竟Linq to SQL的目的,就是为了让我们看不见SQL,避免写复杂的SQL语句,而直接操作实体对象,这样也可以避免程序可读性差、不便于维护。所以除非万不得已,还是不太推荐使用此方法。

2.参考MSDN的资料,采用Linq提供的解决更新冲突的方法,在异常中捕获冲突,然后手动解决冲突:
[code]
try
{
db.SubmitChanges(System.Data.Linq.ConflictMode.ContinueOnConflict);
}
catch (System.Data.Linq.ChangeConflictException ex)
{
foreach (System.Data.Linq.ObjectChangeConflict occ in db.ChangeConflicts)
{
//以下是解决冲突的三种方法,选一种即可

// 使用当前数据库中的值,覆盖Linq缓存中实体对象的值
occ.Resolve(System.Data.Linq.RefreshMode.OverwriteCurrentValues);

// 使用Linq缓存中实体对象的值,覆盖当前数据库中的值
occ.Resolve(System.Data.Linq.RefreshMode.KeepCurrentValues);

// 只更新实体对象中改变的字段的值,其他的保留不变
occ.Resolve(System.Data.Linq.RefreshMode.KeepChanges);
}

// 这个地方要注意,Catch方法中,我们前面只是指明了怎样来解决冲突,这个地方还需要再次提交更新,这样的话,值 //才会提交到数据库。
db.SubmitChanges();
}

[/code]

3. 这个方法也比较简单,也即MSDN中所说的Pessimistic Concurrency Control 。 我们可以来设定哪些字段需要放入Where条件,哪些字段不需要,这样就可以控制更新时候的条件匹配尺度。具体做法,就是在Linq to SQL Designer中,把一些字段的UpdateCheck属性设置为Never,这样,这些字段在更新的时候,就不会再出现在Where条件中了。其实比较推荐的做法,就是在表中设立主键,因为更新的时候,只要把主键作为Where条件,就可以单独的确立一行数据了。把除主键外的字段属性中UpdateCheck设置为Never即可。

关于Linq to SQL中如何管理更改冲突的更多资料,可以在MSDN找到

http://msdn.microsoft.com/zh-cn/library/bb399389.aspx

ASP.net 2.0中水晶报表迁移部署问题

271 views 十月 14, 07 by Timothy

asp.net 2.0的水晶报表,在迁移机器的时候,如果目标机器没有相应的程序集,在IIS中会报错。
错误描述:

Parser Error
Description: An error occurred during the parsing of a resource required to service this
request. Please review the following specific parse error details and modify your source file
appropriately.

Parser Error Message: Could not load file or assembly ‘Microsoft.ReportViewer.WebForms,
Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a’ or one of its
dependencies. The system cannot find the file specified.

【解决方法】

进入你的开发使用机器的c:\windows\assembly目录查看,是否有以下程序集安装在GAC中(如图):

由于资源管理器是使用了shell扩展来查看GAC中安装的程序集,所以我们无法看到这些程序集当前部署的真实目录在什么地方。我们可以直接在命令行模式中进行查看,进入X:\windows\assembly目录,然后查找和水晶报表相关的程序目录(如图):

我们可以大致看到GAC的部署结构了。我们在浏览器中所看到的一个一个的assembly名称,其实对应于assembly目录下面的每一个目录命名。进入具体的目录,还有一级子目录,这个目录的命名,是根据发布到GAC的程序集的版本号加上后面的公钥标记组成。(见上图的下面的红色标记部分)

再进入子目录,我们就可以看到公有部署的程序集,所存在的真正位置了。(如图):

接下来的工作就好办了,拷贝程序集到目标机器上,然后运行gacutil /i filename,将程序集安装到GAC即可。需要拷贝的程序集如下:

1) Microsoft.ReportViewer.Common.dll

2) Microsoft.ReportViewer.ProcessingObjectModel.dll

3) Microsoft.ReportViewer.WebForms.dll

.NET开发常见问题两则

204 views 九月 22, 07 by Timothy

一、关于DateTime
在将DateTime类型,插入到数据库的时候,最容易出现的一种错误:
“SqlDateTime 溢出。必须介于 1/1/1753 12:00:00 AM 和 12/31/9999 11:59:59 PM 之间”
原因是我们在取DateTime.MinValue的值,并插入到数据库的时候,DateTime.MinValue值范围和数据库DateTime类型数据范围不一致造成的。数据库中,DateTime类型字段,最小值1/1/1753 12:00:00,而.NET Framework中,DateTime类型,最小值为1/1/0001 0:00:00,显然,超出了Sql的值的最小范围,导致数据溢出的错误。

解决方法:使用System.Data.SqlTypes.SqlDateTime.MinValue替代System.DateTime类型,这样SqlDateTime的MinValue和Sql中DateTime的范围吻合,就不会再出现以上的错误了。

二、关于跨线程调用控件的问题
在.NET 2.0中,我们常常需要在新建的线程,比如一个工作线程中访问UI控件,程序编译没有任何错误,但是在运行的时候,会抛出异常:InvalidOperationException,并提示消息:“从不是创建控件 control name 的线程访问它。”
原因是因为:访问 Windows 窗体控件本质上不是线程安全的。如果有两个或多个线程操作某一控件的状态,则可能会迫使该控件进入一种不一致的状态。还可能出现其他与线程相关的 bug,包括争用情况和死锁。确保以线程安全方式访问控件非常重要。.NET Framework 有助于在以非线程安全方式访问控件时检测到这一问题。

我们看看有问题的代码:建立新线程,并试图在线程中跨线程调用UI控件
[code]
private void button1_Click(object sender, EventArgs e)
{
Thread thread = new Thread(new ThreadStart(TestFunc));
thread.IsBackground = true;
thread.Start();
}

private void TestFunc()
{
Thread.Sleep(3000);
label1.Text = "ok!";
}

[/code]

这样在运行的时候,就会抛出InvalidOperationException异常。

解决方法:
方法1.关闭跨线程调用控件的检查,来屏蔽掉此异常
需要将Control.CheckForIllegalCrossThreadCalls 设为 false即可。(不推荐)

方法2.使用Control类的Invoke和BeginInvoke
为什么使用Invoke和BeginInvoke可以解决问题呢?因为Control的Invoke和BeginInvoke的参数为delegate,委托的方法是在Control的线程上执行的,也就是我们平时所说的UI线程。而控件也是在UI线程,所以不存在跨线程调用控件的问题。于是我们有了改进的代码:

[code]
private void button1_Click(object sender, EventArgs e)
{
Thread thread = new Thread(new ThreadStart(TestFunc));
thread.IsBackground = true;
thread.Start();
}

private void TestFunc()
{
Thread.Sleep(3000);
this.Invoke(new SetLabel(SetLabelText), new string[] { "ok!" });
}

private delegate void SetLabel(string s);

private void SetLabelText(string s)
{
label1.Text = s;
}

[/code]

建立一个委托,调用Control类的Invoke方法,让UI线程去执行委托实例化的方法。这样就不会出现异常了。

PJBlog恶意广告POST漏洞之测试与修复

225 views 八月 30, 07 by Timothy

今天在网上找了一个Vista的皮肤,赶紧给我的BLOG换上,漂亮了许多,呵呵。
最近BLOG上,广告多得吓人,特别是在文章的评论和留言本里面,真是无孔不入的广告。由于偶比较痛恨这些广告,手动删除也不是办法,整页整页的广告评论和留言,多则上百条。而且手动在管理界面删除,也只是一个治标不治本的办法。
据寡人分析[biggrin],可能是评论和留言的post环节不够强悍,导致垃圾广告有孔而入。不过这个BLOG的评论和留言都是有验证码的,评论的POST对应于blogcomm.asp文件,评论的页面,通过指向GetCode.asp,获取验证码图片,并在Session中保存该验证码,用以和用户输入的验证码比对。
那么如果要达到用程序,自动提交垃圾广告,可以想到的方法有两种:
1.用程序识别图片验证码,获取其内容,然后模拟POST。不过这个比较高难,要程序识别图片中的数字,需要图形识别的知识。
2.绕过验证码的过程,直接POST。这个要根据具体的漏洞而定。

于是随手在google上搜索了一下,PJBlog还真有这个漏洞。[eek]

具体的漏洞描述如下:
blogcomm.asp文件对验证码判断不严格。
如果用户没有请求过GetCode.asp文件,那么服务器端Session里面的GetCode值为空,而用户提交的数据里面验证码也为空,这样刚好空等于空,反而通过了验证码验证。

查看blogcomm.asp源码,在Line 94行有这样的情况。

为了验证这个漏洞是否存在,我们可以自己动手测试一下,用程序模拟POST一次。就拿偶的BLOG做实验吧。首先,随便打开一个文章,启动网络抓包工具,(这里我用的是WSE–WinSocketExpert),随便敲入几个字,查看捕获下来的网络包。我们会发现,POST的数据格式是这样的:

username=Timothy&password=&Message=PostData&logID=171&action=post&submit2=%E5%8F%91%E8%A1%A8%E8%AF%84%E8%AE%BA

username 是评论者名称
password 是密码(游客不需要密码)
logID 就是文章编号
Message 就是评论内容了,垃圾广告最关注的就是这个……
action 是一个隐藏的field,值为post,这个可以在页面上通过查看源码看到
submit2 后面的码,转换成中文,其实就是“发表评论”,同样参考评论页面源码

POST的结构知道了,下面我们用程序来模拟POST一下,看漏洞是否存在,测试代码如下:
(请慎用此部分代码,不要用于垃圾广告软件,谢谢 [cool])

[code]private void PostFunc()
{

WebClient wc = new WebClient();

string postData = "username=Timothy&password=&Message=PostData&logID=171&action=post&submit2=%E5%8F%91%E8%A1%A8%E8%AF%84%E8%AE%BA";
try
{
wc.Headers.Add("Content-Type", "application/x-www-form-urlencoded");
wc.UploadData("http://www.xiaozhou.net/cooldog/blogcomm.asp", "POST", Encoding.Default.GetBytes(postData));
}
catch
{
wc.Dispose();
return;
}

wc.Dispose();
}[/code]

运行此程序,可以在我的blog的编号171的文章评论中,看到下图:

看来漏洞真的存在……
这里,我们可以看到POST数据结构中,logID是可以人为改变的,也就是文章编号。如果把logID作为变量自增,测试代码外面,再套一个for循环,这个程序就成了制造垃圾广告的机器了……

找到了漏洞,得赶紧补上才行。其实我们需要修改的地方,就是在blogcomm.asp。找到94行,这里的判断,由于没有对Session进行判断才导致了此漏洞的产生。
修改方法如下:
blogcomm.asp Line:94
原代码:

IF (memName=empty or blog_validate=true) and cstr(lcase(Session("GetCode")))<>cstr(lcase(validate)) then

替换后的代码,增加对Session的判断:

IF (memName=empty or blog_validate=true) and (cstr(lcase(Session("GetCode")))<>cstr(lcase(validate)) or IsEmpty(Session("GetCode"))) then

大功告成了……[cool]睡觉去……

动手DIY,给你的MSN机器人加上远程控制功能

223 views 八月 28, 07 by Timothy

MSN机器人是一个比较好玩的东东,通过第三方提供的SDK开发包,允许我们开发自己的MSN机器人,实现一些有趣的功能。比如自动聊天等等。

进行MSN机器人开发,需要你首先申请一个MSN帐号,然后去http://sp.incesoft.com/index注册帐号,把你的MSN机器人挂在平台上面。然后下载SDK,开发机器人逻辑。
开发出来的机器人程序,会主动和第三方的平台登录连接,这个时候,你的机器人就可以运行了。

运行的方式:
1.机器人程序–>登录incesoft服务器—>MSN机器人上线
2.MSN客户端向机器人发起聊天—>incesoft接收到数据包—>转发给你的机器人程序

这样,MSN客户端发给你的机器人的信息,就传递到你的机器人程序了,而你可以根据不同的信息,进行不同的动作,这些动作,都是在incesoft提供的SDK里面封装好了。

通过这种运行方式,要加入远程控制功能,是非常方便的,我们可以将登录MSN机器人程序的机器,作为受控端。而且还有一个好处,就是你不用去关心IP变化,只要机器人上线了,就可以开始控制了,这也是用MSN机器人实现的优势。

远程控制,流程如下:
1.被控制机器,运行MSN机器人程序,登录MSN机器人
2.管理机器,发送OP指令给MSN机器人,获取OP控制权限
3.管理机器,发送控制指令
4.被控制机器端,响应指令,做出回应

好像被我说复杂了,其实实现起来非常的简单。

需要准备的工作,申请MSN帐号,然后去http://sp.incesoft.com/index 申请一个帐号,挂上你的MSN帐号。然后下载SDK包,进行开发。

下面是我实现的一个简单的远程控制功能

1.和机器人交互,出现命令菜单

这个时候需要你进入OP模式,才能进行远程控制的操作

2.输入OP密码,这个时候,就获取了OP控制权限,可以控制远程机器了

3.远程控制示例,列举进程列表

4.杀掉指定进程

5.退出OP模式

关机和重启,功能是实现了的,这里就不演示了,呵呵

放上源代码,大家可以下载下来试试

点击下载此文件

调试运行的时候,请替换RobotService.cs Line:144 行的用户名和密码,也就是你在incesoft注册的帐号和密码。

.NET中关机、重启的类

118 views 六月 30, 07 by Timothy

这是一个封装的在.NET下面实现关机和重启的一个类,其实就是直接引用了Win32 API。
业余的时间,写了个MSN机器人,后来突发灵感,想加入远程控制主机的功能,包括重启和关机,于是用上了这个类。待MSN机器人开发完成,放出源码,hoho。

//关机、重启计算机 封装类
using System;
using System.Collections.Generic;
using System.Text;
using System;
using System.Runtime.InteropServices;

namespace MyRobot
{

class ShoutDown
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct TokPriv1Luid
{
public int Count;
public long Luid;
public int Attr;
}

[DllImport("kernel32.dll", ExactSpelling = true)]
internal static extern IntPtr GetCurrentProcess();

[DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
internal static extern bool OpenProcessToken(IntPtr h, int acc, ref IntPtr phtok);

[DllImport("advapi32.dll", SetLastError = true)]
internal static extern bool LookupPrivilegeValue(string host, string name, ref long pluid);

[DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
internal static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall,
ref TokPriv1Luid newst, int len, IntPtr prev, IntPtr relen);

[DllImport("user32.dll", ExactSpelling = true, SetLastError = true)]
internal static extern bool ExitWindowsEx(int flg, int rea);

internal const int SE_PRIVILEGE_ENABLED = 0×00000002;
internal const int TOKEN_QUERY = 0×00000008;
internal const int TOKEN_ADJUST_PRIVILEGES = 0×00000020;
internal const string SE_SHUTDOWN_NAME = "SeShutdownPrivilege";
internal const int EWX_LOGOFF = 0×00000000;
internal const int EWX_SHUTDOWN = 0×00000001;
internal const int EWX_REBOOT = 0×00000002;
internal const int EWX_FORCE = 0×00000004;
internal const int EWX_POWEROFF = 0×00000008;
internal const int EWX_FORCEIFHUNG = 0×00000010;

public static void DoExitWin(int flg)
{
bool ok;
TokPriv1Luid tp;
IntPtr hproc = GetCurrentProcess();
IntPtr htok = IntPtr.Zero;
ok = OpenProcessToken(hproc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref htok);
tp.Count = 1;
tp.Luid = 0;
tp.Attr = SE_PRIVILEGE_ENABLED;
ok = LookupPrivilegeValue(null, SE_SHUTDOWN_NAME, ref tp.Luid);
ok = AdjustTokenPrivileges(htok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero);
ok = ExitWindowsEx(flg, 0);
}
}
}

利用IIS作为宿主 发布你的WCF Service

378 views 三月 16, 07 by Timothy

最近公司的一个需求,涉及到WCF开发。在网上找了些资料,大都是利用单独的应用程序、或者Windows服务作为WCF Service的host。其实WCF还提供一种方式,和以前的Remoting比较类似,就是基于IIS发布你的WCF Service。

大致部署步骤如下:

1.编译好WCF Service的Class Library,确保没有错误。
2.建立.svc文件,内容格式如下:

1<%@ServiceHost language=c# Debug="true" Service="CommonService.MailService" %>   

 
这里的Service,是你的Service名称,及其所属命名空间.

为了确保IIS能正确识别.svc文件,需要在IIS属性中检查,如果没有.svc类型,需要手动添加,如图:

3.在IIS发布WCF Service,将其目录设置为Web共享,并注意在IIS中,为该虚拟目录打开匿名访问的权限。
4.在工程中,添加Web.config文件。这里需要建立WCF Service服务段的配置信息,我们可以直接利用MS提供的配置工具方便进行配置,如图:

打开配置工具,根据配置向导,选择好Service Type 和Contract信息。在选择服务通讯方式的时候,我们需要选择http通讯方式(因为我们服务的宿主是IIS,所以应选择http方式)。在EndPoint中,填入Endpoint的地址。再选择添加一个Endpoint,address为mex,类型为mexHttpBinding,Contract填入IMetadataExchange。

5.为了能在IIS中测试发布的效果,我们需要允许从客户端通过http方式获取元数据,这样的选项WCF默认是false,所以我们还需要一点小设置:
在Service Behavior节点,单击右键,新建一个Service Behavior,单击Add按钮,添加一个Extention Position,名称为serviceMetadata,如图:

并在上面双击,在窗口中,把HttpGetEnabled选项设置为true,如图:

一切的准备工作就绪后,我们就可以通过IIS来访问测试我们的WCF Service了。如下图:

红色框中的部分,是我们设置了HttpGetEnabled的结果,否则是不会出现这一行的,它允许我们从客户端以WSDL方式获取其源数据。
小结一下:采用IIS作为宿主程序,方便之处就在于你不必专门为WCF Service再去建立一个host文件。

.NET中实现自绘的ListBox

180 views 十月 30, 06 by Timothy

在平常的开发过程中,我们为了改善用户的体验,通常会在界面上花一些功夫。因为清爽的界面,通常给用户带来一些耳目一新的感觉,并且能增强用户满意度。前几周的一个项目中,就用到了ListBox,需要突出用户在ListBox中选中的项,而.NET自带的ListBox由于配色过于普通,无法满足我们的需要,这就需要我们重新对ListBox的配色进行一些修改。
在Windows的消息机制中,有一个消息叫做WM_DRAWITEM,当控件(比如:Button,ComboBox,ListBox,Menu)需要被重新绘制的时候,会向该控件的父窗口发送这个消息。父窗口通过响应这个消息,就可以实现对该控件外观的绘制,也就是说,通过响应WM_DRAWITEM消息,我们可以接管系统对控件的绘制,这样,我们就可以随心所欲,用我们喜欢的方式,来绘制这个控件了。听起来不错吧?
.NET的WinForm程序,也是基于Windows的GUI界面的窗口,而.NET追溯到最底层,其实也是对Win32 API的更高一个层次的封装。所以,.NET的WinForm程序同样也逃脱不了消息机制。于是我们就可以在程序中,通过设置控件为Owner Draw属性,然后加入对WM_DRAWITEM的处理,就可以在OnDrawItem函数中,定制控件外观了。
理论上就是这样,我们开始动手吧,打造一个具有个性的ListBox控件!需要说明的是,当我们把控件属性中的DrawMode改为OwnerDrawFixed过后,.NET以及将绘制这个控件的责任交给了我们,因此我们必须要实现DrawItem函数,否则ListBox会是一片白,什么也看不到。
1.建立一个简单的工程,在工具箱拖放ListBox到Form上,然后将ListBox属性中的DrawMode改为OwnerDrawFixed,或者在Form_Load中,设置控件的属性:
this.listBox1.DrawMode = DrawMode.OwnerDrawFixed;
2.在事件窗口中,为DrawItem添加处理函数。
3.配色方案:这里,我们设定被选中的项目有一个蓝色的外框,并且选中的项中,内容呈亮绿色显示,背景色呈灰色显示,这样就更醒目一些了。
4.最后一步,就是加入OnDrawItem的代码,如下:

private void listBox1_DrawItem(object sender, System.Windows.Forms.DrawItemEventArgs e)
{
//定义背景色,选中为灰色,未选中为白色
Brush brNoSelectedBack=Brushes.White;
Brush brSelectedBack=Brushes.Gray;
//定义前景色,选中为亮绿色,未选中为黑色
Brush brSelectedFore=Brushes.LightGreen;
Brush brNoSelectedFore=Brushes.Black;

//定义焦点,有焦点状态下绘制蓝色外框(蓝色)
Pen penFocus=new Pen(Brushes.Blue);

e.Graphics.FillRectangle(Brushes.White,e.Bounds);

//绘制Item被选中的情况
if((e.State & DrawItemState.Selected) == DrawItemState.Selected)
{
e.Graphics.FillRectangle(brSelectedBack,e.Bounds);
//绘制焦点外框
e.Graphics.DrawRectangle(penFocus,e.Bounds.X,e.Bounds.Y,e.Bounds.Width,e.Bounds.Height-1);
e.Graphics.DrawString(this.listBox1.Items[e.Index].ToString(),
this.listBox1.Font,brSelectedFore,e.Bounds);
//e.DrawFocusRectangle();
return;
}

//绘制没有被选中的情况
e.Graphics.FillRectangle(brNoSelectedBack,e.Bounds);
e.Graphics.DrawString(this.listBox1.Items[e.Index].ToString(),
this.listBox1.Font,brNoSelectedFore,e.Bounds);
}

到这里,一个简单的自绘ListBox就实现了,不过这里的代码都是直接加载父窗体的里面,不是很美观。其实我们可以自己实现一个这样的类,并把这个ListBox编译为一个.NET控件,这样,在其他的项目中就可以很简单的重用这个控件了。:)
下面是这个自绘控件的效果,左边是我们的自绘ListBox,右边是普通的ListBox。等有时间,我会把这些封装成为一个dll控件 :)。这里提供源代码的下载,有兴趣的朋友可以再根据自己的需求进行一些自己的修改。

点击下载此文件

Page: 3 of 4 1 2 3 4