我的 WinClock 项目系列之三 (Mediator模式的应用)

在开始本次之前,有必要对上次的一点儿遗漏的地方再废话一下。
    关机的实现:关机也是调用Windows API 实现的,具体代码参考上次列出的清单 
    PInvokeService.DoExitWin(int) 函数。
值得一提的是 关机对话框调用了 Windows 中一个未公开的 API, 我们不知道这个函数
    的名字,但是我们知道它的地址,我使用 ISO C++ 进行了封装,代码如下:
 1    #include <windows.h>
 2
 3    namespace CPPCode {
 4        namespace Shutdown {
 5            typedef int (CALLBACK *SHUTDOWNDLG) (int); 
 6
 7            extern "C" {
 8                __declspec(dllexport) void ShowShutdownDialog();
 9            }
 
10         
11            void ShowShutdownDialog() 
12                HINSTANCE hInst = LoadLibrary(LPCWSTR(L"shell32.dll")); 
13                SHUTDOWNDLG SHShutDownDialog; 
14
15                if(hInst != NULL)  
16                    // Shutdown dialog API is not publicized by Microsoft but we know
17                    // It's ID is 60
18                    SHShutDownDialog = (SHUTDOWNDLG)GetProcAddress(hInst, (LPSTR)60); 
19                    SHShutDownDialog(0); 
20                    FreeLibrary(hInst);
21                }

22            }
 
23        }

24    }

25

    编译生成的 dll 是 CPPCode.Shutdown.dll
    然后你可以在 PInvokeService.cs 里面找到这样的代码:
    [DllImport("CPPCode.Shutdown.dll", ExactSpelling = true, SetLastError = true)]
    public static extern void ShowShutdownDialog();

抛开具体细节讨论抽象,现在可以开始正式讲本次的内容了。

Mediator模式的应用
    动机(Motivation):
      在软件的构建过程中,经常会出现多个对象互相关联交互的情况,对象之间常常会维持一种
    复杂的引用关系,如果遇到一些需求的更改,这种直接的引用关系将面临不断的变化。
   
      在这种情况下,我们可以使用一个“中介对象”来管理对象间的关联关系,避免相互的对象之间
    紧耦合引用关系,从而更好地抵御变化。
   
    意图(Intent):
      用一个中介对象来封装一系列对象交互。中介者使各对象不需要显示的相互引用,从而使其
    耦合松散,而且可以独立地改变它们之间的交互。
                                                                    ————《设计模式》GOF
                                                                   
我们抛开这些精典的理论,看看如何把我们的功能细节分离到一系列 class 中,以及让菜单工作。
先总体上看一下最后的类图:

                                      点击下载完整类图

     Element 和 Mediator 都是抽象类,但他们之间存在很强的耦合关系, Element 依赖 Mediator 对象,
同时Mediator对象内部包含 Element 的集合。因此 Mediator 也依赖 Element,Element 了类化就
可以在这些类中实现我们的功能细节。那就先看看这两个抽象类吧:

  1    // Element.cs
  2    internal abstract class Element : IDisposable {
  3        private ICommand theCommand;
  4        protected Mediator mediator;
  5
  6        public Element(Mediator mediator) {
  7            this.mediator = mediator;
  8            this.mediator.AddElement(this);
  9        }

 10
 11        ~Element() {
 12            Dispose(false);
 13        }

 14
 15        protected MainForm mainForm {
 16            get {
 17                return mediator.MainForm;
 18            }

 19        }

 20
 21        protected ICommand command {
 22            get {
 23                return theCommand;
 24            }

 25            set {
 26                theCommand = value;
 27                if (theCommand != null{
 28                    RegisterEvents(false);
 29                    RegisterEvents(true);
 30                }

 31            }

 32        }

 33
 34        public object Source {
 35            get {
 36                return this.command.Source;
 37            }

 38        }

 39
 40        protected void NotifyStatusChanged() {
 41            if (mediator != null{
 42                mediator.Notify();
 43            }

 44        }

 45
 46        protected ClockOption clockOpt {
 47            get {
 48                return mainForm.ClockOption;
 49            }

 50        }

 51
 52        protected RemindOperate remindOperate {
 53            get {
 54                return mainForm.RemindOperate;
 55            }

 56        }

 57
 58        // Only dispose the item, do not dispose the mediator
 59        // because the mediator is sigalton, it need dispose 
 60        // only once
 61        protected virtual void Dispose(bool disposing) {
 62            if (disposing) {
 63                // Do not move this outside of the if block, because
 64                // memory of the element may have been collected by GC
 65                try {
 66                    RegisterEvents(false);
 67                    this.command.Dispose();
 68                }
 finally {
 69                    this.command = null;
 70                }

 71            }

 72        }

 73
 74        protected abstract void OnExecute();
 75        protected internal virtual void OnStatusChanged() {
 76        }

 77
 78        private void RegisterEvents(bool register) {
 79            if (register) {
 80                this.command.Execute += CommandOnExecuted;
 81            }
 else {
 82                this.command.Execute -= CommandOnExecuted;
 83            }

 84        }

 85
 86        private void CommandOnExecuted(object sender, EventArgs args) {
 87            OnExecute();
 88            NotifyStatusChanged();
 89        }

 90
 91        IDisposable Members
 99    }

100    
101    // Mediator.cs
102    internal abstract class Mediator : IDisposable {
103        private MainForm mainForm;
104        protected IList<Element> elementList;
105
106        public Mediator(MainForm mainForm) {
107            this.elementList = new List<Element>();
108            this.mainForm = mainForm;
109        }

110
111        ~Mediator() {
112            Dispose(false);
113        }

114
115        public MainForm MainForm {
116            get {
117                return this.mainForm;
118            }

119        }

120
121        public IList<Element> ElementList {
122            get {
123                return this.elementList;
124            }

125        }

126
127        // Notify all other elements to change status
128        public abstract void Notify(Element sorceElement);
129
130        // Notify all elements to change status
131        public abstract void Notify();
132
133        public virtual void AddElement(Element element) {
134            // Mainmenu status maybe depended on submenu, So Mainmenu Element should 
135            // be placed after submenu Element
136            elementList.Insert(0, element);
137        }

138
139        protected virtual void Dispose(bool disposing) {
140            if (disposing) {
141                try {
142                    foreach (Element element in this.elementList) {
143                        element.Dispose();
144                    }

145                }
 finally {
146                    elementList.Clear();
147                    elementList = null;
148                }

149            }

150        }

151
152        IDisposable Members
160    }

    在这里,MainForm 是主窗口,它充当了中介,它可以被 Element 和 Mediator 引用。
    你可能已经发现,Element还依赖于一个 ICommand 接口,IComand 接口引用了一个 object 的对象
    在它的一个实现类 MenuItemCommand 中,这个对象指向的是一个 ToolStripMenuItem 的对象。

 1    // ICommand.cs
 2    public interface ICommand : IDisposable {
 3        object Source {
 4            get;
 5        }

 6
 7        event EventHandler Execute; 
 8    }

 9    
10    // MenuItemCommand.cs
11    internal class MenuItemCommand : ICommand {
12        private ToolStripMenuItem source;
13        protected EventHandlerList Events;
14        private static readonly object EventExecute = new object();
15
16        public MenuItemCommand(ToolStripMenuItem source) {
17            this.Events = new EventHandlerList();
18            this.source = source;
19            this.source.Click += SourceOnClick;
20        }

21
22        ~MenuItemCommand() {
23            Dispose(false);
24        }

25
26        private void SourceOnClick(object sender, EventArgs args) {
27            OnExecute(EventArgs.Empty);
28        }

29
30        protected virtual void Dispose(bool disposing) {
31            if (disposing) {
32                this.source.Click -= SourceOnClick;           
33            }

34        }

35
36        protected virtual void OnExecute(EventArgs args) {
37            EventHandler handler = this.Events[EventExecute] as EventHandler;
38            if (handler != null{
39                handler(this, args);
40            }

41        }

42
43        ICommand Members
61
62        IDisposable Members
70    }

    那么你可能会问,Element 为什么不直接依赖 ToolStripMenuItem 呢?其实在这个项目里,我们完全
    可以这么做,因为我们要使用的仅仅是ToolStripMenuItem, 我们之所以抽象出来一个 ICommand, 完全是
    考虑到可能会用到其他的UI组件比如 Button 等,那么如果有这样的需求,很简单,我们可以实现一个
    ButtonCommand,让他实现 ICommand 接口,这样保证了接口的统一,它就可以和 ToolStripMenuItem 一起
    工作了。简单地说,抽象一个 ICommand 只是为了统一接口。Element 和 Mediator 还实现了 IDispose
    接口,这是.Net中被经常提到的 Dispose 模式,如果感兴趣,你可以看看我的另一篇博客:
     .Net Dispose 模式 与 C++/CLI 确定性资源清理
    实际使用中,Dispose 方法都是通过 Mediator 对象调用的。
   
    好的,抽象已经完成,看看具体怎么实现各个 Menu 的工作细节吧。举一个穿透桌面功能的例子吧:

 1    internal class PenetrateElement : Element {
 2        public PenetrateElement(Mediator mediator, ToolStripMenuItem source)
 3            : base(mediator) {
 4            base.command = new MenuItemCommand(source);
 5        }

 6
 7        protected override void OnExecute() {
 8            clockOpt.Penetrate = !clockOpt.Penetrate;
 9
10            byte alpha = (byte)(mainForm.Opacity * 255);
11            if (clockOpt.Penetrate) {
12                PenetrateService.MousePenetrate(mainForm, alpha);
13            }
 else {
14                PenetrateService.MouseNotPenetrate(mainForm, alpha);
15            }

16        }

17
18        protected internal override void OnStatusChanged() {
19            ToolStripMenuItem menuItem = base.Source as ToolStripMenuItem;
20            menuItem.Checked = clockOpt.Penetrate;
21        }

22    }

    OnExecute 是点击菜单是会执行的方法,OnStatusChanged 的调用可能是因为用户单击了任何一个菜单,
    也可能是MainForm中的代码调用了 mediator.Notify(), 由于 Mediator 中引用着所有 Element 的集合,
    所以调用 Notify() 将导致所有 Element 的 OnStatusChanged() 被调用,同样,直接点击任何一个菜单项,
    也会导致其他(还有自身)菜单接到通知,即 OnStatusChanged() 被调用。所以这些都是在抽象基类里面完成
    的,实体 Element 只需要处理这两个方法就可以了。实体 Mediator 本软件中只有一个,他的实现很简单。

 1    // ContextMenuMediator.cs
 2    internal sealed class ContextMenuMediator : Mediator {
 3        public ContextMenuMediator(MainForm mainForm)
 4            : base(mainForm) {
 5        }

 6
 7        public override void Notify(Element sorceElement) {
 8            // elementList may be null because it has been disposed
 9            if (elementList != null{
10                foreach (Element element in elementList) {
11                    if (!object.ReferenceEquals(element, sorceElement)) {
12                        element.OnStatusChanged();
13                    }

14                }

15            }

16        }

17
18        public override void Notify() {
19            // elementList may be null because it has been disposed
20            if (elementList != null
21                foreach (Element element in elementList) {
22                    element.OnStatusChanged();
23                }

24            }

25        }

26    }

27

    为了更好地利用 Visual Studio 的 Designer 设计器,菜单还是在 MainForm 里面用 Designer 生成的,只是在
    MainForm 的构造行函数里面,菜单项被 Element 引用。由于进行了设计,功能都被分散了,MainForm 里面的实体
    代码并不多,看看吧。

  1    public partial class MainForm : Form, IMementoCapable {
  2        private ClockOption clockOpt;
  3        private RemindOperate remindOperate;
  4        private Mediator mediator;
  5        private Point mousePosition;
  6        private Bitmap currentBitmap;
  7        private WindowShapeMaker windowShapeMaker;
  8
  9        private static readonly int WM_KEYDOWN = 0x0100;
 10        internal static readonly string AppPath = ClockOption.AppPath;
 11
 12        public MainForm(ClockOption clockOpt) {
 13            InitializeComponent();
 14            EnableDoubleBuffering();
 15            this.clockOpt = clockOpt;
 16            remindOperate = new RemindOperate();
 17            Core.Properties properties = PropertyService.Get<Core.Properties>("WinClock.RemindOperate"new Core.Properties());
 18            remindOperate.SetMemento(properties);
 19
 20            this.mousePosition = Point.Empty;
 21            this.Cursor = Cursors.SizeAll;
 22            windowShapeMaker = new WindowShapeMaker(this, clockOpt);
 23        }

 24
 25        internal ClockOption ClockOption {
 26            get {
 27                return this.clockOpt;
 28            }

 29        }

 30
 31        internal RemindOperate RemindOperate {
 32            get {
 33                return this.remindOperate;
 34            }

 35        }

 36
 37        internal void RefreshSkin() {
 38            string path = Path.Combine(Path.GetDirectoryName(typeof(MainForm).Assembly.Location), "Images");
 39            string filenaeme = Path.Combine(path, clockOpt.Filename);
 40
 41            try {
 42                if (!File.Exists(filenaeme)) {
 43                    clockOpt.Filename = "Default.png";
 44                    filenaeme = Path.Combine(path, clockOpt.Filename);
 45                }

 46
 47                if (currentBitmap != null{
 48                    currentBitmap.Dispose();
 49                }

 50                currentBitmap = new Bitmap(filenaeme);
 51                RefreshWindow();
 52            }
 catch (Exception e) {
 53                using (EventLog elog = new EventLog()) {
 54                    elog.Log = "Application";
 55                    elog.Source = "WinClock";
 56                    elog.WriteEntry("Error: " + e.Message, EventLogEntryType.Error);
 57                }

 58                Environment.FailFast(ResourceService.GetString("CSharpCode.WinClock.MainForm.LackComponent"));
 59            }

 60        }

 61
 62        internal void RefreshWindow() {
 63            windowShapeMaker.RefreshWindow(currentBitmap);
 64        }

 65
 66        internal void CheckBounds(ref Point location) {
 67            if (clockOpt.CheckBounds) {
 68                Rectangle rectScreen = Screen.GetWorkingArea(this);
 69                if (location.X < rectScreen.Left) {
 70                    location.X = rectScreen.Left;
 71                }
 else if (location.X + this.ClientSize.Width > rectScreen.Right) {
 72                    location.X = rectScreen.Right - this.ClientSize.Width;
 73                }

 74
 75                if (location.Y < rectScreen.Top) {
 76                    location.Y = rectScreen.Top;
 77                }
 else if (location.Y + this.ClientSize.Height > rectScreen.Bottom) {
 78                    location.Y = rectScreen.Bottom - this.ClientSize.Height;
 79                }

 80            }

 81        }

 82
 83        protected override void OnLoad(EventArgs e) {
 84            base.OnLoad(e);
 85            SetupMenuItemElements();
 86            mediator.Notify();
 87            byte alpha = clockOpt.Opacity;
 88            if (clockOpt.Penetrate) {
 89                PenetrateService.MousePenetrate(this, alpha);
 90            }
 else {
 91                PenetrateService.MouseNotPenetrate(this, alpha);
 92            }

 93            RefreshSkin();
 94        }

 95
 96        protected override void OnMouseMove(MouseEventArgs e) {
 97            if (e.Button == MouseButtons.Left) {
 98                // The clock is fixed up on the desktop
 99                if (!clockOpt.CanMove)
100                    return;
101
102                int left = this.Location.X + e.Location.X - this.mousePosition.X;
103                int top = this.Location.Y + e.Location.Y - this.mousePosition.Y;
104                Point location = new Point(left, top);
105                CheckBounds(ref location);
106                this.SetBounds(location.X, location.Y, this.ClientSize.Width, this.ClientSize.Height);
107                clockOpt.Location = this.Location;
108            }

109
110            base.OnMouseMove(e);
111        }

112
113        protected override void OnMouseDown(MouseEventArgs e) {
114            if (e.Button == MouseButtons.Left) {
115                this.mousePosition = e.Location;
116            }

117
118            base.OnMouseDown(e);
119        }

120
121        protected override CreateParams CreateParams {
122            get {
123                CreateParams createParams = base.CreateParams;
124                createParams.ExStyle |= PInvokeService.WS_EX_LAYERED;
125                return createParams;
126            }

127        }

128
129        protected override void OnMouseLeave(EventArgs e) {
130            clockOpt.PreviewOpacity = clockOpt.Opacity;
131            windowShapeMaker.RefreshWindow(currentBitmap);
132
133            base.OnMouseLeave(e);
134        }

135
136        protected override void OnMouseEnter(EventArgs e) {
137            clockOpt.PreviewOpacity = clockOpt.MouseEnterOpacity;
138            windowShapeMaker.RefreshWindow(currentBitmap);
139
140            base.OnMouseEnter(e);
141        }

142
143        // Press ESC to exit
144        protected override bool ProcessCmdKey(ref Message msg, Keys keyData) {
145            if (msg.Msg == WM_KEYDOWN && keyData == Keys.Escape) {
146                this.Close();
147                return true;
148            }

149
150            return base.ProcessCmdKey(ref msg, keyData);
151        }

152
153        protected override void OnClosed(EventArgs e) {
154            base.OnClosed(e);
155            Core.Properties properties = this.CreateMemento();
156            PropertyService.Set("WinClock.MainForm", properties);
157            properties = clockOpt.CreateMemento();
158            PropertyService.Set("WinClock.ClockOption", properties);
159        }

160
161        private void SetupMenuItemElements() {
162            mediator = new ContextMenuMediator(this);
163            new OptionElement(mediator, menuItemOption);
164            new RemindElement(mediator, menuItemRemind);
165            new CalendarElement(mediator, menuItemCalendar);
166            new AboutElement(mediator, menuItemAbout);
167            new PenetrateElement(mediator, menuItemPenetrate);
168            new UnmovableElement(mediator, menuItemUnmovable);
169            new TopMostElement(mediator, menuItemTopMost);
170            new MinimizeElement(mediator, menuItemMiniMize);
171            new ExitElement(mediator, menuItemExit);
172
173            // Additional function
174            new NotepadElement(mediator, menuItemNotePad);
175            new CalcElement(mediator, menuItemCalc);
176            new MediaPlayerElement(mediator, menuItemMediaPlayer);
177            new BackupElement(mediator, menuItemBackup);
178            new DirectXElement(mediator, menuItemDirectx);
179            new SystemDoctorElement(mediator, menuItemDoctor);
180
181            // Shutdown windows
182            new ShutdownElement(mediator, menuItemShutDown);
183
184            // Language
185            new LanguageElement(mediator, menuItemLanguage);
186        }

187
188        private void NotifyIconOnDoubleClick(object sender, EventArgs e) {
189            this.Activate();
190        }

191
192        private void NotifyIconOnMouseDown(object sender, MouseEventArgs e) {
193            // Right click to active the window
194            if (e.Button == MouseButtons.Right) {
195                this.Activate();
196            }

197        }

198
199        private void NotifyIconOnMouseMove(object sender, MouseEventArgs e) {
200            // Show the tooltip
201            string time = DateTime.Now.ToString(Core.RemindOperate.YearMonthDayTimeFmt);
202            string week = Core.RemindOperate.Days[DateTime.Now.DayOfWeek];
203            if (string.Equals("zh-CHS", clockOpt.Language)) {
204                string lunar = ChineaseDateService.GetLunarCalendar(DateTime.Now.Date);
205                notifyIcon.Text = string.Format("{1} {2}{0}农历{3}", Environment.NewLine, time, week, lunar);
206            }
 else {
207                notifyIcon.Text = string.Format("{0} {1}", time, week);
208            }

209        }

210
211        private void TimerSecondOnTick(object sender, EventArgs e) {
212            windowShapeMaker.RefreshWindow(currentBitmap);
213        }

214
215        private void TimerRemindOnTick(object sender, EventArgs e) {
216            remindOperate.CheckRemindList();
217        }

218
219        private void EnableDoubleBuffering() {
220            this.SetStyle(ControlStyles.OptimizedDoubleBuffer &
221                ControlStyles.UserPaint &
222                ControlStyles.AllPaintingInWmPaint,
223                true);
224
225            this.UpdateStyles();
226        }

227        
228        protected override void Dispose(bool disposing) {
229            if (disposing) {
230                IDisposable[] resourceList = new IDisposable[] {
231                    mediator,
232                    currentBitmap,
233                    windowShapeMaker, 
234                    components
235                }
;
236                foreach (IDisposable resource in resourceList) {
237                    if (resource != null{
238                        resource.Dispose();
239                    }

240                }

241            }

242            base.Dispose(disposing);
243        }

244
245        IMementoCapable Members
265    }

    这里 Designer 生成的代码没有列出来,其中 SetupMenuItemElements 方法就是完成了实例化 Element 和 Mediator
    具体类的作用, 这样他们都被 medator 引用,所以你不用担心它马上会被垃圾收集干掉。

参考资料: 
    
关机对话框
    李建忠-C#面向对象模式纵横谈第七讲-(行为型模式)  Mediator 中介者模式

      


posted on 2008-06-04 18:25  优哉@游哉  阅读(2562)  评论(8编辑  收藏  举报

导航