• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • YouClaw
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录

Dotnet之旅

  • 博客园
  • 联系
  • 订阅
  • 管理

公告

View Post

用.NET打造自己的个人信息管理中心——5

 

用.NET打造自己的个人信息管理中心——5

PersonalInfo软件开发过程实录

      在《系统分析与设计》一文中,我们介绍了整个软件的总体架构,在《开发数据库存取对象》一文中,我们封装了一个类OLEDBAccessObj用于访问数据库,在《开发自定义树控件》一文中,我们又得到了一个功能强大的自定义树控件SuperTreeView,有了前面的基础,现在是装配我们软件的时侯了。

一、装配程序

1.设计用户界面

       首先,我们需要设计一个用户界面,开发前我在纸上画出了以下图形:

 

图 1

       整个用户界面被分成了四块,其中树与工作区两块是可以动态改变相互大小的,因为树可能有很多层,占据不小的屏幕空间。工具栏区也是这样,现在只有一个工具栏,说不定以后还会多加一个,所以也要能动态改变大小。VS.net提供了一个Spliter控件,可以作为分隔条使用。

       每个区域都放一个Panel组件作为容器,以后可以不断地往里头加控件,这比将控件直接放在窗体上要好得多了。

       要实现动态改变窗格大小,关键在于正确设计Panel的Dock属性。请注意,虽然图1中只能看到四个区域,但实际上我放了5个Panel,中间其实是一个大的Panel,其上又放了左右两个面板,分别容纳SuperTreeView控件和三种不同类型的窗体(参见《系统分析与设计》一文的图4、图5和图6)。三个窗体都各有一个Panel面板控件,所有的文本框、按钮等都放在Panel上,并要正确设置好它们的Anchor属性,之所以这样,是因为这三个窗体的所有控件都会被嵌入到图1的工作区中去的,因而控件必然会重定位。

       说了这么多,关键还是需要亲手做一做。如果读者您能把我这个程序的总体界面成功地做出来,相信您一定对.NET所提供的Dock、Anchor属性印象深刻,真是太方便了。

       在这里插句嘴:这个界面实现思路其实是从Java GUI设计中的BorderLayout布局学来的,也算是“偷师”吧,.NET技术和Java技术实在太象了!

       界面设计好以后,就开始分别开发三个工作窗体,之后,把SuperTreeView控件加入到主窗体中。然后,给树增加快捷菜单,给主窗体增加工具栏。

       先实现了树节点的增删改功能,这实际上就是直接调用SuperTreeViewNode控件的对应功能,接着,必须实现点击树节点在工作区嵌入不同窗体的功能。

2 处理树节点点击事件

     

  我在程序初始化时就创建了三个窗体对象,分别保存在三个变量中。这样,需要时就可以免去动态创建窗体的系统开销。

       当用户点击树节点时,TreeView控件的AfterSelected事件发生,在此事件处理代码中将这三个窗体中的Panel的Parent属性设置为主窗体工作区中的Panel,就把这三个窗体“嵌入”到了工作区中。

       除了OnlyText类型的节点,所有其他类型节点的数据都保存在数据库中,怎样设计按节点信息从数据库中提取对应数据的流程呢?

       我的想法是利用每个节点的Tag属性存放对应的三种实体对象(DetailText,OnlyFile和Folder,一个节点显然只能有一个实体对象),当用户点击节点时,从Tag属性中取出对象,按照节点类型将其传给对应的窗体,由窗体负责把这个对象正确地显示窗体的各种控件中。

       那么,每个树节点的Tag属性在什么时候装入对象呢?当然可以在从XML文件中创建树时就访问数据库,创建对应的对象并放到Tag属性中,但这样一来程序启动就太慢了,事实上,每次用户所使用的只不过是树中的部分节点,为了这部分节点而把所有节点的信息都从数据库中装入,显然是其蠢无比的。

       较好的方案是当用户点击节点时,检查节点的Tag属性是不是为空,为空的时候则根据节点的完整路径从数据库中提取信息创建对象,放入Tag属性中。下次再访问此节点就不需要再访问数据库了,从而提高了性能。

3 解决节点移动带来的画面闪烁问题

      

当节点移动时,我发现右边的工作区面板闪烁得历害,怎么回事?

       原来我在在能terSelectedlyFile实现节点移动功能是通过先删除选中节点再插入的方式完成的,在这个过程中AfterSelected事件也会激发,会再次设置对应窗体中面板的Parent属性,这就是造成闪烁问题的罪魁祸首,但事实上节点移动时由于选中的节点始终是同一个节点,因而AfterSelected事件是不必要的,但我们又不能“取消”这一事件,我只好设了一个“开关”变量:

     '是否处于移动状态?

    Private IsInNodeMove As Boolean = False

       此变量为True时表明正处于移动状态。看看AfterSelected事件的处理代码:

 

Private Sub SuperTreeView1_AfterSelect(ByVal sender As System.Object, ByVal e As System.Windows.Forms.TreeViewEventArgs) Handles SuperTreeView1.AfterSelect

        If Me.IsInNodeMove = False Then

            Me.OnAfterSelectedNode(e.Node)  '引发点击事件处理流程

        End If

    End Sub

End Sub

 

       在移动节点开始时设Me.IsInNodeMove =True,结束时再设为false,就可以避免面板闪烁问题。

       在AfterSelect事件处理代码中有一个方法OnAfterSelectedNode()实际完成所有功能,这种不在具体事件中写大量处理代码,而是将其封装为一个方法,在事件处理代码中只写方法名字,是一种很有用的编程习惯,可以提高代码的可维护性。

       另外树本身在移动节点时开始也闪烁得厉害,因为移动节点时会引发节点的删除与加入,并需要更新数据库,节点有上千个时树控件反应就慢一点,可以通过使用树控件的BeginUpdate()和EndUpdate()方法降低闪烁到可以忍受的程度。

       但即使用上了我所说的所有手段,移动节点时我觉得程序反应还是慢,读者可以在我的基础上分析一下看看有没有更好的方法提高性能,我一时还未找到更好的方法。

二、可怕的BUG排除

      

编程序是种乐趣,但找BUG绝对是件痛苦的事。

       下面讲讲我在开发过程中更正一个重大BUG的过程。

       PersonalInfo软件中,三个嵌入的窗体是相对独立的,每个窗体都有一个保存按钮,例如处理DetailText类型节点的窗体如下所示:

图 2 处理DetailText型节点数据的原始窗体

       当我在图2的文本框中键入内容,单击“保存”时,所有内容都会被存到数据库中,这一切都很正常。

       但在实际运行中,这个窗体是被嵌入到主窗体的工作区中的,当用户写了一点内容之后,点击树的其它节点,如果他没有在切换到其它节点之前点击本窗体的保存按钮,则所有数据都会丢失,这显然很不好。应该由程序自动检测这种情况,然后自动保存。

       怎样自动检测这一过程呢?

       我注意到用户在输入内容时,文本框是有焦点的,当他点击树节点时,树获得了焦点,文本框失去焦点。所以,可以在文本框的LostFocus事件中保存数据。

图 3 树获得焦点,文本框失去焦点

       于是我这么做了,但奇怪的是,当我在文本框中输入内容,点击其它树节点,再点回原节点,则原来输入的东西都无影无踪了!怎么回事?

       仔细想一想:

       每个DetailText类型的树节点都关联着一个DetailText对象,当用户点击此节点时,此对象被取出传给窗体frmDetailText(通过向窗体的属性DetailTextObject赋值实现),窗体的ShowObjInForm方法负责将此对象的Text属性内容显示在文本框txtDetailText中。

       用户单击保存按钮时,调用了UpdateDB()方法将对象写回到数据库中:

 

  Private DetailSaver As new DetailTextAccessObj

  Public Sub UpdateDB()

         ‘更新对象值

         obj.Text = Me.txtDetailText.Text

        '存入数据库中

         Me.DetailSaver.UpdateDBRow(obj)

    End Sub

 

       上文中的obj就是存放窗体公有属性DetailTextObject值的DetailText对象。

       在窗体的LostFocus()过程是这么写的:

 

Private Sub txtDetailText_LostFocus(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtDetailText.LostFocus

             Me.UpdateDB()

 End Sub

 

       当文本框失去焦点时,调用UpdateDB更新数据库,在UpdateDB中先用用户在文本框txtDetailtext中新输入的值更新对象,再调用DetailText数据存取对象DetailSaver将DetailText对象写入数据库,只有几行代码,逻辑也很清楚,没问题啊?!

       当时试了又试,错误总是依旧!真是郁闷!

       突然想到,会不会是点击节点的事件处理代码中的问题,于是到主窗体中找到点击节点事件的处理代码:

 

      ……

        Dim obj As DetailText

        If node.Tag Is Nothing Then

            '从数据库中创建对象,并装入Tag属性中

            obj = Me.DetailReader.GetObjectByPathStr(node.FullPath)

            node.Tag = obj

        Else

            obj = node.Tag

        End If

        '显示相关信息

        Me.DetailForm.DetailTextObject = obj

          ……

 

       从上面的代码可以看出,点击节点事件发生时先检查树的Tag属性,如果为空,则从数据库中创建DetailText对象,然后将此对象传给frmDetailText类型的窗体变量DetailForm。

在frmDetailText的DetailTextObject属性中,其代码是这样写的:

 

     Private obj As DetailText = Nothing

    Public Property DetailTextObject() As DetailText

        Get

            Return obj

        End Get

        Set(ByVal Value As DetailText)

                obj = Value

                Me.ShowObjInForm()   ‘将DetailText对象内容显示在窗体的控件中

          End Set

End Property

 

       一旦对象传送到窗体后,用户就可以通过文本框来修改这一对象的Text属性,改完后点击其它节点,在文本框的LostFocus事件中再调用UpdateDB()来将用户输入存到数据库中。

       真的一切都没问题啊!为什么当用户这么操作时其内容会无影无踪?

       记得当时弄到这里,已是深夜2点多钟了,累得不行了,只好静一静,泡了杯咖啡。

       后来突然灵光一闪,会不会是事件引发次序的问题?

       我原来是这么想的:

 

用户点击其它节点à文本框的LostFocus事件发生,更新数据库à树节点点击选中事件AfterSelected发生,传送新的对象并更新窗体显示。

      

      

如果最后两步换一下位置,则老对象还没保存,就传入了新对象,结果保存的是新对象内容!这可能就是问题关键所在!

       为了跟踪事件引发的顺序,我在有可能关联的事件处理代码中加入了Console.WriteLine()语句,当使用VS.net运行程序时,将可以在其输出窗口中看到输出(其实更专业的跟踪方法是使用事件日志,但在这个小程序中,使用VS.net跟踪就足够了)。

       程序结果证实了我的判断,真实的事件是这样发生的:

 

用户点击其它节点à树节点点击选中事件AfterSelected发生à文本框的LostFocus事件发生!

 

       由于LostFocus事件发生的滞后,UpdateDB()保存的obj对象已是由树节点AfterSelected事件中传入的新对象,原来节点对象的内容没被保存,自然会丢失了!

       真相大白!

       但.NET窗体事件发生的顺序我们是不能改变的,怎么解决?

       我想到的办法是在改变窗体的DetailTextObject对象之前,先保存老对象,于是修改 frmDetailText的DetailTextObject属性代码如下:

 

     Private obj As DetailText = Nothing

    Public Property DetailTextObject() As DetailText

        Get

            Return obj

        End Get

        Set(ByVal Value As DetailText)

            '用户做了改变

            If Not (obj Is Nothing) Then

                Me.UpdateDB()

            End If

            If Not (Value Is Nothing) Then

                obj = Value

                Me.ShowObjInForm()

            End If

        End Set

    End Property

 

       注意上面粗体的部分!之所以要判断obj是否为Nothing,是因为用户第一次点击树时,Obj对象为Nothing。

       代码改过之后,运行程序,发现一切正常!

       但这样一来,每次用户点击一个节点,程序都得访问一次数据库,虽然我这个程序是单机程序,而且现在的主流机型都是P4的CPU,512M的内存,但这么写代码也有点太那个了吧!

       得想法减少对数据库的访问次数!

       最简单的方法是:如果对象改变了数值,则应该保存,否则,就不应该写入到数据库中。于是,我给DetailText对象增加了一个属性:

 

    '用于区分是否已更改过,保存之后,由外部对象重置为False

    Public HasChanged As Boolean = False

 

       再修改其Text属性:

 

     Private _text As String = ""

    Public Property Text() As String

        Get

            Return _text

        End Get

        Set(ByVal Value As String)

            If _text <> Value Then

                _text = Value

                HasChanged = True

           End If

        End Set

    End Property

 

现在,对象本身就“知道”自己是否更改过了。再修改窗体中的UpdateDB():

 

Public Sub UpdateDB()

       ‘更新对象值

         obj.Text = Me.txtDetailText.Text

        '存入数据库中

        If obj.HasChanged Then

            Me.DetailSaver.UpdateDBRow(obj)

            obj.HasChanged = False

        End If

 End Sub

 

       这样一来,访问数据库的次数至少减少了80%!

       得意之余,突然又发现了一个新问题:

       如果我正在文本框中输入内容时,直接点击关闭按钮退出程序,则下次再进来的时候,我最后输入的内容节点数据又没了!

       我差不多要崩溃了!难道我的方案是错的?但实在不甘心啊!于是鼓起剩勇再追穷寇!

       是不是程序直接退出时文本框的LostFocus事件没发生?通过使用Console.Writeln()跟踪,发现这个事件的确是发生了,UpdataDB()也成功执行了,那是什么原因丢失数据?仔细跟踪,终于发现在这种情况下文本框的LostFocus事件发生之时,文本框的Text属性为空!而UpdataDB()代码中有一句:

     obj.Text = Me.txtDetailText.Text

      

       这相当于清空了原对象的内容!

       问题找到了,解决方案就很简单了,在文本框的TextChange事件中更新对象Text属性就行了:

 

Private Sub txtDetailText_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles txtDetailText.TextChanged

        obj.Text = Me.txtDetailText.Text

End Sub

 

       之后,就可以从UpdateDB()中移除obj.Text = Me.txtDetailText.Text这句了。

       终于一切搞掂,抬头一看,已是繁星满天,凌晨三点多了!!

       我不禁感叹:这就是软件开发者的生活。所以许多程序员都是黑着眼圈,形容憔悴,就因为熬夜熬多了。象我,当时调完这个程序BUG后,足足睡了两天才把精力给恢复过来(好惨!)。

       这让你恨之入骨但又欲罢不能的软件开发!

三、开发感悟

      

软件写完了,就可以总结总结了。

       整个PersonalInfo软件共有7000多行代码,我在工作之余断断续续地写了一个月,为了开发调试方便,我把全部源代码放到了同一个EXE中,这对于这个单人开发的小软件来说是可行的,但在正式的团队协作的软件开发当中,一定要分成几个DLL,由不同的人负责开发,特别是SuperTreeView控件,只有位于类库中才可以被以后的工程所引用,这个工作就由读者自行完成吧。

       在软件开发过程中,我深深地感到把握面向对象编程理论的重要性。

       一个很重要的经验就是在Windows Form类型程序的开发当中,事件激发的次序实在太重要了!

       另外一个经验就是在分块开发、组合装配这个开发过程中,分块的组件可能没有问题,但合在一起就会引发新的问题,所以,程序的集成测试绝对是必要的。

       这个软件自从开发完毕,我就一直使用它来管理我的各种资料,充当了备忘录、日记本、心得感悟、书籍写作大纲设计等多项工作,使用起来还是非常方便的,所以才想到要把这个软件介绍给大家,并提供全部源代码,希望能对大家有点启发与帮助。

       还有些功能在文章中没有介绍,比如对树和数据库的备份与恢复功能,模拟Windows资源管理器的前进与后退功能,对文本框内容的打印等等,读者可以自行阅读对应的源代码。

       事实上,还有许多功能是可以加入的,比如:

l       对单个树节点的密码保护

l       可以打印树结构

l       对指定节点或子树数据的导入与导出

l       支持键盘操作(如Del删除节点)树,允许后悔功能

l       直接对树节点的类型进行更改,比如从OnlyFile类型转为DetailText类型

l       提供在树中拖动移动节点的功能

l       ……

       在界面上,我并没有花太多的时间,没有加入很多很“酷”的第三方界面控件,简洁是我所需要的。大家所看到的那个惊涛巨浪的图是我随便从网上挑出来加入的,纯粹是为了装点一下界面,没什么大用,大家可以用你自己喜欢的图片覆盖程序文件夹下的Background.JPG,甚至写些代码,象Windows桌面一样,可以方便地换背景。

       毫无疑问,在性能上还有许多可以改进的地方,由于在移动节点时需要及时更新数据库和使用较慢的XMLDocument保存树,所以在移动节点时反应速度较慢,但我已尽可能地减少了移动过程中整个树的闪烁现象。另外,我并没有指望用这个小工具来取代Windows资源管理器,对于大于10M的文件,不适合将它们全部放到数据库中,尤其是Access这种性能有限的桌面型数据库,会使Access文件尺寸增长很快。说一个真实的场景:当我在我的奔四笔记本上把一个38M的文件装入数据库时,我不得不长久地等待,硬盘灯闪得让我心焦,另外,偶而Access会报告我它忙于“Fetching”,从而无法保存指定的文件……,因此,不要用它来保存巨大的文件(比如一部DVD电影),如果你真有这个需要,请使用这个小工具保存它的文件路径字串,而不是文件本身。提醒一下,ACESS提供了一个压缩数据库的功能,压缩之后不仅ACCESS数据库文件本身“减肥”了,数据存取速度也可以加快。需要的话,可以在我们的程序中通过COM接口直接调用ACCESS来完成这一压缩功能。但考虑到这个功能用得不多,而且我不想让这个.NET小工具与老的技术COM有太多的关联,所以没把这个功能加入到程序中。

       整个软件写了不少行代码,其中我自己也知道有不少地方是可以优化和调整的(比如有一定量的重复代码),但写完第一版本之后,这个小工具已能满足我的日常需要,所以我就偷懒不再改进它了。如果哪位朋友正在研究“重构”理论,那么,我想我这个小工具应该是您练手的最佳实例之一,经过重构及性能优化,代码量一定会减少,而速度会有较大的提高。

       毫无疑问,整个软件还显得很粗糙,欢迎大家对这个小工具进行改进与完善,如果更正了BUG,或是增加了很酷的新功能,别忘了给我也寄一份哟!

(注:本软件源代码请到CSDN杂志频道下载:http://mag.csdn.net)

posted on 2005-05-19 09:26  浮游  阅读(674)  评论(0)    收藏  举报

刷新页面返回顶部
 
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3