[原创]Silverlight的弹出窗口--设计

在上一篇《[原创]Silverlight的弹出窗口--展示》中,仅仅是将我制作的基于Silverlight的弹出窗口作了一个功能性的展示,并提供了一个非常基础的版本的源码。

确实这个版本非常基础,其中存在着众多未经优化的代码,同时结构也存在着一定程度的混乱,因此如果不从整体上对其进行梳理,仅仅通过阅读代码恐怕是很难了解其整个工作过程的。

此篇的目的就是,从设计的结构上,对弹出窗口这一功能作一个大致的介绍,将贯穿于整个作品的设计思想描绘出来,而这思想也必然是整作项目中最为稳定的部分,在将来不会产生太大的改动。

源码已经有了小部分的更新,如果上一次有下载的园友,请重新下载

整体结构

首先应当从整体上对这个项目的结构有一个鸟瞰的概念,因此附上架构图一份,当然这并不是正规的UML图,但应当足以说明不少问题

Untitled

可以看到,在这个结构中,存在着6个非常重要的组件,他们分别是:

  1. PopupService:核心组件,用于提供弹出窗口的功能,所有弹出窗口都由其进行控制
  2. LayoutMask:位于弹出窗口下部的遮罩层,同时也将提供模态对话框打开时屏蔽下层控件的功能
  3. PopupBox:弹出窗口的基类,与LayoutMask之间具有一定的通信和交互能力,同时提供特效等功能
  4. BoxPage/MessagePage:PopupBox的实现,分别对应不同的功能,但基于此设计,并不存在太多与实现相关的代码,因此不会作为重点进行讲述
  5. DragService:管理窗口的鼠标拖动功能,从外部引入拖动功能有利于展现与功能的解耦,同时所有与鼠标拖动相关的实现集中于一个类中,方便了BUG出现之后的定位与修改

PopupService

在我的设计中,我们不能通过直接的新建对象,如BoxPage box = new BoxPage();来生成一个弹出窗口。

当然最初的设计确实是可以使用new来创建的,但在日后的测试中发现,这样的创建会产生很多问题,在下面例举一二:

1. 每一个弹出窗口会拥有一个遮罩层,因此后开的弹出窗口一定比之前的在更上层,这就导致先打开的弹出窗口永远不可能被移到最上方,而在日常操作中,点击后面的窗口应当可以将这个窗口移到最前方

2. 过多的遮罩层导致了资源大量的占用,当打开5个以上弹出窗口时,应用程序的响应将出现明显变慢的现象

这种种缺陷,都将矛头指向了一处,即我们需要一个统一的环境来管理弹出窗口及其遮罩层,保证多个弹出窗口所拥有的遮罩层的“单例性”,因此就产生了通过PopupService这样一个“服务”来管理的策略,其管理方案如下:

1. 一个PopupService对应一个Control,此Control就是弹出窗口的拥有者

2. PopupService会产生一个遮罩层与此Control联系,并且在此后有弹出窗口时都会使用这唯一的一个遮罩层

3. 无论何时,对于同一个Control,将只会有一个PopupService进行服务

看到上面的方案后,我想大多数人都会发现,PopupService变成了“伪单例”或“局部单例”的类,因此就着“单例模式”的设计,我们肯定不能简单地通过new的构造来使用这个类,因此就引出了该类的一个静态方法GetServiceFor,此方法接收一个参数即PopupService的拥有者,当然为了能够将遮罩层铺在控件上,我们要求控件是一个布局控件,即Panel类型。

为了保证PopupService的伪单例的特性,必须将生成的对象保存起来,这里简单地用到了Dictionary进行保存,当调用GetServiceFor方法时,首先在缓存池中寻找,如果找不到则调用new进行实例化,随后放入缓存池中进行持久的保存,以保证日后不会再发生重新构造的问题,其具体代码如下:

public static PopupService GetServiceFor(Panel owner)
{
if (!Cache.ContainsKey(owner))
{
PopupService service = new PopupService(owner);
Cache[owner] = service;
}
return Cache[owner];
}

 

在这里并没有显式地对Cache对象(Dictionary<Panel, PopupService>类型)进行线程锁,这是因为Silverlight与Winform类似,所有对UI的操作只能在主线程进行,因此我认为没有必要在一个单线程执行的环境中进行加锁。

上面讲了PopupService的产生过程,下面例举一下PopupService提供的方法,由于方法十分简单,就不展开讨论,其方法主要有3个:

1. GetBoxPage:获取在此PopupService管理下的BoxPage的一个对象,有重载

2. GetMessagePage:获取在此PopupService管理下的MessagePage的一个对象,有重载

3. RegisterPopupBox:对已经创建好的但没有纳入PopupService管理的PopupBox对象进行注册,注册后的对象将由此PopupService进行管理,即分配一个LayoutMask

LayoutMask

LayoutMask听起来就叫“遮罩层”,但其实他不是一个控件,其地位类似于PopupService,是一个“管理者”的角色,他将管理多个PopupBox,从而将弹出窗口于PopupService分享开来,起到解耦的作用,尽可能地减少PopupService的负担,从而使程序结构更加清晰。

而从界面的展现角度来讲,又可以认为LayoutMask确实是一个“控件”,因为他会生成一个Canvas平铺于其所有者(Panel类型)之上,此Canvas就是真正的“遮罩层”,对Canvas设定背景色就会产生模态对话框的效果,同时所有的弹出窗口都将作为这个Canvas的子元素,通过ZIndex的改变来确定哪一个对话框处在最前端,从而得以模仿我们日常使用中“点击在后面的窗口之后窗口会被置于最前端”的效果。

LayoutMask提供了与弹出窗口的管理相关的若干方法,其主要对外的方法如下:

1. AddBox:将一个弹出窗口添加进来,当然在调用窗口的Show方法之前,窗口是不会显示出来的,AddBox只是将窗口与本体进行联系。

2. RemoveBox:与AddBox相反,将一个窗口从本容器中除去,此后即使调用窗口的Show方法,窗口也不会再显示出来了,因此被移除的Box在XAML树中已经是一个孤岛,与XAML根没有联系的元素是不可能被渲染的。

3. PositionBox:在上一个版本中称为CenterBox,在此版本中加入了新的功能,即连续打开窗口时,窗口不会叠在一起,而会按一定的偏移量相互错开,因此方法也被改名为PositionBox,其作用就是找到一个合适的位置来放置弹出窗口。

除了公开的方法之外,其部分内部方法也有着举足轻重的作用:

1. CheckModal:每当AddBox或RemoveBox调用时,都会重新检查是否有弹出窗口是模态的,如果在这个LayoutMask管理的弹出对话框中有一个或多个是模态的,则需要将作为遮罩层使用的Canvas改为模态以屏蔽下面的其他控件,其方式是简单地给Canvas加上背景色。

2. MaxZIndex:返回管理的所有弹出对话框的ZIndex中的最大值,这方便了在后层的对话框移到前层,只需要设定其ZIndex为MaxZIndex + 1即可。

3. ReorderZIndex:当然ZIndex是有最大值的,一个很熟悉的数字65535,所以如果不断地给窗口增加ZIndex,必将导致ZIndex超出范围,这当然不是我们所希望的结果。因此就有了一个方法,当ZIndex已经过大的时候,将所有控件的ZIndex进行重新排列,按照现有的窗口叠放次序,从1开始重新排列ZIndex,保证ZIndex永远不会超过最大值(当然你硬要连续打开65536个窗口我也真没办法……)

4. RenderMask:当所有弹出窗口关闭时,遮罩层也应该相应消失,而当遮罩层未打开时,也不能打开新的窗口,所以这里就有一个流程,在第一个窗口打开时需要先将遮罩层打开,因此有了RenderMask方法,负责将遮罩层加入到所有者的子元素中并显示出来。

DragService:

DragService是一个辅助类,他将提供窗口的鼠标拖动功能,这个类的结构也非常简单,在构造的时候将需要窗口的对象传递给他,随后便可通过设定IsDraggable属性来打开/关闭鼠标拖动的功能,对于鼠标拖动的实现,在网上无论是FLEX,JS,WINFORM还是Silverlight都有很多的实现了,我使用的也与这些大同小异,主要就是通过监听MouseDown,MouseUp,MouseMove这3个事件来实现,可以看一下IsDraggable的实现:

public bool IsDraggable
{
get
{
return m_IsDraggable;
}
set
{
if (m_IsDraggable == value)
{
return;
}
if (value)
{
EnableDrag();
}
else
{
DisableDrag();
}
}
}

当IsDraggable被设定为true时,会调用EnableDrag方法,此方法如下:

private void EnableDrag()
{
PopupBox.DragMouseCaptureArea.MouseLeftButtonDown +=
new MouseButtonEventHandler(Drag_MouseLeftButtonDown);
PopupBox.DragMouseCaptureArea.MouseMove +=
new MouseEventHandler(Drag_MouseMove);
PopupBox.DragMouseCaptureArea.MouseLeftButtonUp +=
new MouseButtonEventHandler(Drag_MouseLeftButtonUp);
}

 

此方法监听了3个事件,对于拖动的逻辑,是在MouseDown的时候设定“开始拖动”,在MouseMove的时候,如果已经“开始拖动”,则计算出鼠标移动的距离,使窗口移动同样的距离,最后在MouseUp的时候“停止拖动”,具体代码可以在源文件中找到,不再浪费此处的空间了~~

而当IsDraggable设为false的时候,则调用DisableDrag方法,方法如下:

private void DisableDrag()
{
PopupBox.DragMouseCaptureArea.MouseLeftButtonDown -= Drag_MouseLeftButtonDown;
PopupBox.DragMouseCaptureArea.MouseMove -= Drag_MouseMove;
PopupBox.DragMouseCaptureArea.MouseLeftButtonUp -= Drag_MouseLeftButtonUp;
}

 

通过注销3个事件,自然也就停止了拖动的功能。

以后

下一篇将会比较详细地去讲述一下LayoutMask中的一些方法的实现,同时讲述诸如动画效果等复杂内容的实现,最后介绍如何扩展这个框架来实现自定义的PopupBox。

下载源码

posted @ 2009-02-24 22:31  Gray Zhang  阅读(5617)  评论(5编辑  收藏  举报