Introduction

This article describes the construction of a countdown timer application written in C# and WPF, using Laurent Bugnion's MVVMLight Toolkit. This article is based on the work of some previous articles I've written:

As usual most of the code shown in this article is elided, particularly since no-one likes scrolling through five screens worth of XAML to see one line of interest. Please download the source zip file to see the whole thing.

This was written, and more importantly, tested, on a Windows 7, 64-bit box, but uses the .NET 4.0 framework, so should 'just work'!

Requirements and Features

The Countdown Timer is going to be relatively simple:

  • The starting time can be chosen by the user.
  • Notify the user visually (in the application and task bar) and audibly.
  • The application has settings that the user can change.
  • The Windows 7 taskbar icon shows the progress of the timer.

The original motivation for this was that I came across the Pomodoro Technique whilst browsing the web and thought it would be fun to write a countdown timer that could be used for this. It is, in short, a 'getting things done' idea which can be boiled down to:

  • work for 25 minutes
  • break for 5 minutes
  • repeat

So I decided that the default setting for the timer is 25 minutes, and that it should record the number of completed countdowns unobtrusively, should someone wish to use this application in that way.

Choosing the Underlying Timer

We use the WPF DispatchTimer to perform a count. We are not making any guarantees about the accuracy of the timer.

In fact neither does the documentation:

Timers are not guaranteed to execute exactly when the time interval occurs, but they are guaranteed to not execute before the time interval occurs. This is because DispatcherTimer operations are placed on the Dispatcher queue like other operations. When the DispatcherTimer operation executes is dependent on the other jobs in the queue and their priorities.

By leveraging the .NET Framework, we use a TimeSpan that allows us to increment, and importantly, decrement by a specified amount. We then simply decrement our starting value every time the DispatchTimer ticks, until we get a negative TimeSpan, and then we stop.

The code is written in such a way that the TimerModel is just a concrete implementation of ITimerModel and the concrete instantiation of an ITimerModel is generated from a single factory method: in other words, you could write your own ITimerModel derived class instead and update the factory method as required (e.g., useSystem.Threading.Timer instead).

MVVM

Since this is a WPF application, we will use the MVVM pattern to layout the code.

What does this mean? The application will be divided into:

  • Views - XAML-only layouts that the application uses: i.e., the GUI and all its windows!
  • ViewModels - translate the data between the Views and the Models.
  • Models - the actual code that does the work (and everything else).

If you find this confusing or want to know more, please see another of my articles: WPF/MVVM Quick Start Tutorial.

Application Settings

All applications have settings and this one is no different: to persist the application's settings, we take advantage of the class System.Configuration.ApplicationSettingsBase. This is subclassed for the WPF application when you create it, so you can then just address the application settings programmatically, for example:

Hide   Copy Code
_timer.Duration = Properties.Settings.Default.Duration;

where we have created a Duration property.

In the same way that we hide the implementation of the Timer behind a ITimerModel interface, we also use an interface called ISettingsModel, and use a concrete instance called SettingsModel, along with a builder method to retrieve an instance of the class. This gives us the option, as before, to change the settings backing store, to something else in the future (ini file anyone?).

Updating Settings Between Application Versions

To cater for updates to the application, we can use the following method: define UpgradeRequired in our settings, and set to True by default. We then use:

Hide   Copy Code
if (Properties.Settings.Default.UpgradeRequired)
{
Properties.Settings.Default.Upgrade();
Properties.Settings.Default.UpgradeRequired = false;
Properties.Settings.Default.Save();
}

to force the upgrade of the application settings only when the UpgradeRequired flag is true. For newly versioned assemblies, all settings take their default values, this code is triggered, and the settings are copied from a previous application version, if it exists, to the new one.

It is worth noting that for this 'trick' to work, you always need to define this field in your application settings inthe very first version of your application.

The Views and ViewModels

The application has several views that are all UserControls and hosted in the MainWindow. This means no pop-up dialogs! They are:

  • The main TimerView
  • The SettingsView
  • The AboutView

with the corresponding ViewModels:

  • The TimerViewModel
  • The SettingsViewModel
  • The AboutViewModel

Changing Views and Messaging

As we want to use 'separation of concerns', or 'encapsulation' (if you prefer), we do not want view-models to communicate directly. In order to do this, we simply use message passing, in other words:

The MVVMLight Toolkit provides us with a singleton Messenger class that we can register message consumers and message producers with. So to raise an 'event' in one view model from another, we simple pass a message, for example:

Hide   Copy Code
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
// Lastly, listen for messages from other view models.
Messenger.Default.Register<SimpleMessage>(this, ConsumeMessage);
} private void ConsumeMessage(SimpleMessage message)
{
switch (message.Type)
{
case MessageType.TimerTick:
WindowTitle = message.Message;
break;
// ....
}
}
}

and in the TimerViewModel:

Hide   Copy Code
public class TimerViewModel : ViewModelBase
{
private void OnTick(object sender, TimerModelEventArgs e)
{
Messenger.Default.Send(new SimpleMessage(MessageType.TimerTick, TimerValue));
}
}

What this achieves is as follows: the TimerViewModel updates the TimerView countdown clock in the main window's ContentControl, but we want to update the window's title to also show the countdown. The main window View is bound to the MainViewModel, so to do this, and to keep the view-models separate, we pass a message containing the time remaining. The reason we update the window title bar is discussed a little later.

The TaskBar Preview and the Window Title

As you can see in this screenshot:

The countdown value is shown in the taskbar item thumbnail, and in the main window's title. The reason we update the window's title is that when a window is minimzed, the taskbar item thumbnail is not updated by Windows, so if you were to hover your mouse pointer over the icon on the task bar when the item is minimized, the thumbnail preview will display the countdown at the time you minimized the window. Fortunately, the title of the window is updated in the thumbnail preview, so we ensure that we update that to provide a visual clue to the user.

TaskBar Messages

We need a second message type to communicate task bar progress updates in Windows 7: since theMainWindow 'view' is bound to the MainViewModel, we need to receive messages from the TimerViewModelthat are appropriate to update the task bar progress indicator. Fortunately this is relatively straightforward, and once again we make use of the Messenger.Default.Register and Messenger.Default.Send pattern we saw earlier.

The second message class is simply:

Hide   Copy Code
public class TaskbarItemMessage
{
public TaskbarItemMessage()
{
State = TaskbarItemProgressState.None;
Value = -1.0;
}
public TaskbarItemProgressState State { get; set; } public double Value { get; set; } public bool HasValue { get { return ! (Value < 0.0); } }
}

Our TimerViewModel just sends instances of these messages and the MainViewModel receives them, and via the magic of data-binding, between the view model (MainViewModel) and the view (MainWindow) the taskbar progress indicator just updates:

Hide   Copy Code
<Window x:Class="Btl.MainWindow"
DataContext="{Binding Main,
Source={StaticResource Locator}}">
<Window.TaskbarItemInfo>
<TaskbarItemInfo ProgressState="{Binding ProgressState}"
ProgressValue="{Binding ProgressValue}">
<TaskbarItemInfo.ThumbButtonInfos>
<ThumbButtonInfoCollection>
<ThumbButtonInfo Command="{Binding PlayCommand}"
Description="Start"
DismissWhenClicked="False"
ImageSource="Resources\icon.play.png" />
<ThumbButtonInfo Command="{Binding PauseCommand}"
Description="Pause"
DismissWhenClicked="False"
ImageSource="Resources\icon.pause.png" />
</ThumbButtonInfoCollection>
</TaskbarItemInfo.ThumbButtonInfos>
</TaskbarItemInfo>
</Window.TaskbarItemInfo>
<!-- ELIDED -->
</Window>

Since the TaskBarItemInfo thumbnail previews offer us more than just the preview, we can add a thumbnail 'Start' and 'Pause' button (just like Media Player), so we can control the countdown timer from the thumbnail preview, hence the ThumbButtonInfo elements above.

A Note on the UI Design

There is some method to the madness of the Countdown Timer UI: since the Play and Pause buttons are likely to be the most used, they are the largest, then the settings and reset buttons are smaller so they are less likely to be clicked on. The 'About' window is accessed by a small '?' in the bottom right hand corner.

Similarly, the 'OK' and 'Cancel' buttons are widely separated in the Settings view so it is clear which one you want to click on.

And lastly, aside from the button icons (play, pause, etc.), I've left the theming of the application alone, so that the OS can choose how to theme it. Of course, since this is an MVVM application, you can take the source code, fire up Blend, and change it however you like.

There are even some third-party libraries that will do a lot of the work for you, e.g., MahApps.Metro.

A WPF/MVVM Countdown Timer的更多相关文章

  1. WPF MVVM 验证

    WPF MVVM(Caliburn.Micro) 数据验证 书接前文 前文中仅是WPF验证中的一种,我们暂且称之为View端的验证(因为其验证规是写在Xaml文件中的). 还有一种我们称之为Model ...

  2. jQuery Countdown Timer 倒计时效果

    这个一款简单的 jQuery 倒计时插件,用于显示剩余的天数,小时,分钟和秒.倒计时功能是非常有用的一个小功能,可以告诉用户多久以后您的网站将会发布或者关闭进行维护,还可以用于举办活动的开始和停止的倒 ...

  3. WPF MVVM初体验

    首先MVVM设计模式的结构, Views: 由Window/Page/UserControl等构成,通过DataBinding与ViewModels建立关联: ViewModels:由一组命令,可以绑 ...

  4. WPF MVVM实现TreeView

    今天有点时间,做个小例子WPF MVVM 实现TreeView 只是一个思路大家可以自由扩展 文章最后给出了源码下载地址 图1   图2     模版加上了一个checkbox,选中父类的checkb ...

  5. WPF/MVVM 快速开始指南(译)(转)

    WPF/MVVM 快速开始指南(译) 本篇文章是Barry Lapthorn创作的,感觉写得很好,翻译一下,做个纪念.由于英文水平实在太烂,所以翻译有错或者译得不好的地方请多指正.另外由于原文是针对W ...

  6. 使用Prism提供的类实现WPF MVVM点餐Demo

    使用Prism提供的类实现WPF MVVM点餐Demo 由于公司开发的技术需求,近期在学习MVVM模式开发WPF应用程序.进过一段时间的学习,感受到:学习MVVM模式,最好的方法就是用MVVM做几个D ...

  7. WPF MVVM使用prism4.1搭建

    WPF MVVM使用prism4.1搭建 MVVM即Model-View-ViewModel,MVVM模式与MVP(Model-View-Presenter)模式相似,主要目的是分离视图(View)和 ...

  8. ViewModel从未如此清爽 - 轻量级WPF MVVM框架Stylet

    Stylet是我最近发现的一个WPF MVVM框架, 在博客园上搜了一下, 相关的文章基本没有, 所以写了这个入门的文章推荐给大家. Stylet是受Caliburn Micro项目的启发, 所以借鉴 ...

  9. WPF MVVM 架构 Step By Step(6)(把actions从view model解耦)

    到现在为止,我们创建了一个简单的MVVM的例子,包含了实现了的属性和命令.我们现在有这样一个包含了例如textbox类似的输入元素的视图,textbox用绑定来和view model联系,像点击but ...

随机推荐

  1. ruby中的模块

    什么是模块 模块(module)是Ruby特有的功能之一.类用来表现具有数据与行为(程序)的"东西", 而模块大致来说,则是只有程序部分的集合体.类与模块最大的不同在于: 1.模块 ...

  2. firefox同步数据时无响应问题

    之前设置了firefox的数据同步,可以在不同电脑上,同步自己的书签等信息,感觉很方便实用,最近在点工具立即同步时,不报错,书签也没有同步,没有任何响应: 后来查了许多网上资料,都不见效,无意间看到 ...

  3. birdnest是什么意思_birdnest在线翻译_英语_读音_用法_例句_海词词典

    birdnest是什么意思_birdnest在线翻译_英语_读音_用法_例句_海词词典 birdnest

  4. SE 2014年4月22日(一)

    实验 练习: 如图配置: 两自治系统 AS 100  和 AS 200 AS 100 是由两私有自治系统 (AS 65001 和 AS 65002)构成 要求配置BGP联盟 使得 R3 R4 R5 下 ...

  5. [置顶] 我的Android进阶之旅------>如何将Android源码导入Eclipse中来查看(非常实用)

    Android源码下载完成的目录结构如如所示: step1:将.classpath文件拷贝到源代码的根目录 Android源码支持多种IDE,如果是针对APP层做开发的话,建议大家使用Eclipse开 ...

  6. struts(三)——struts框架实现登录示例

    前两篇讲解了struts框架的基本实现,自己感觉讲的也有些枯燥,今天拿登录的例子来做一个实现. 1.新建一个javaweb项目,并将struts的jar包拷贝到WebRoot/WEB-INF/lib下 ...

  7. Cordova CLI源码分析(三)——初始化

    本部分主要涉及以下三个文件 1 cli.js 2 cordova.js 3 events.js 通过前一篇package.json的分析,可以知道,当命令行执行cordova相关命令时,首先调用mai ...

  8. Linux下忘记rootpassword怎么办?

    1.首先确认server出于安全的状态.也就是没有人可以随意地连接MySQL数据库. 由于在又一次设置MySQL的rootpassword的期间,MySQL数据库全然出于没有password保护的  ...

  9. 09_android入门_採用android-async-http开源项目的GET方式或POST方式实现登陆案例

    依据08_android入门_android-async-http开源项目介绍及用法的介绍,我们通过最常见的登陆案例进行介绍android-async-http开源项目中有关类的使用.希望对你学习an ...

  10. 在MyEclipse中编写Web Project,编码设置全集合

    1.window-->preference-->general-->content type 然后在<Content Types>中展开每一颗子项,并在<Defau ...