WPF/MVVM 倒计时器






4.95/5 (19投票s)
使用 MVVM 模式在 WPF 中实现的倒计时器
引言
本文介绍了使用 C# 和 WPF 编写的倒计时器应用程序的构建过程,该应用程序使用了 Laurent Bugnion 的 MVVMLight Toolkit。本文基于我之前撰写的一些文章。
与往常一样,本文中的大部分代码都省略了,尤其因为没有人喜欢滚动五屏 XAML 来查看一行感兴趣的代码。请下载源 zip 文件以查看完整内容。
本文是在 Windows 7 64 位系统上编写并更重要的是经过测试的,但使用了 .NET 4.0 框架,所以应该“直接可用”!
要求和功能
倒计时器将相对简单
- 用户可以选择开始时间。
- 以视觉(应用程序和任务栏)和声音方式通知用户。
- 该应用程序有用户可以更改的设置。
- Windows 7 任务栏图标显示计时器的进度。
最初的动机是我在浏览网页时偶然发现了 番茄工作法,并认为编写一个可以用于此的倒计时器会很有趣。简而言之,这是一种“把事情做好”的思想,可以归结为:
- 工作 25 分钟
- 休息 5 分钟
- 重复
所以我决定计时器的默认设置为 25 分钟,并且应该以不显眼的方式记录完成的倒计时次数,以便有人希望以这种方式使用此应用程序。
选择底层计时器
我们使用 WPF 的 DispatchTimer
来执行计数。我们不保证 计时器 的准确性。
事实上,文档也没有
计时器不保证在时间间隔发生时准确执行,但保证不会在时间间隔发生之前执行。这是因为
DispatcherTimer
的操作与其他操作一样被放入 Dispatcher 队列中。DispatcherTimer
操作的执行取决于队列中的其他作业及其优先级。
通过利用 .NET Framework,我们使用 TimeSpan
,它允许我们按指定量递增,尤其重要的是递减。然后,我们只需在 DispatchTimer
滴答时递减我们的起始值,直到我们得到负的 TimeSpan
,然后停止。
代码的编写方式是,TimerModel
只是 ITimerModel
的一个具体实现,而 ITimerModel
的具体实例化是从单个工厂方法生成的:换句话说,您可以编写自己的 ITimerModel
派生类,并根据需要更新工厂方法(例如,改用 System.Threading.Timer
)。
MVVM
由于这是一个 WPF 应用程序,我们将使用 MVVM 模式来布局代码。
这是什么意思?应用程序将被划分为:
- 视图 - 仅 XAML 的布局,应用程序使用:即 GUI 及其所有窗口!
- 视图模型 - 在视图和模型之间传递数据。
- 模型 - 执行工作(以及其他所有工作)的实际代码。
如果您发现这令人困惑或想了解更多信息,请参阅我的另一篇文章:WPF/MVVM 快速入门教程。
应用程序设置
所有应用程序都有设置,这个也不例外:为了持久化应用程序的设置,我们利用了 System.Configuration.ApplicationSettingsBase
类。当您创建 WPF 应用程序时,它会被继承,因此您可以像这样通过编程方式直接访问应用程序设置:
_timer.Duration = Properties.Settings.Default.Duration;
在这里,我们创建了一个 Duration
属性。
与我们通过 ITimerModel
接口隐藏 Timer
的实现一样,我们也使用了一个名为 ISettingsModel
的接口,并使用了一个名为 SettingsModel
的具体实例,以及一个构建方法来检索该类的实例。这使我们有机会像以前一样,将设置的后端存储更改为其他内容(例如 ini 文件?)。
更新应用程序版本之间的设置
为了应对应用程序的更新,我们可以使用以下方法:在我们的设置中定义 UpgradeRequired
,并默认设置为 True
。然后我们使用
if (Properties.Settings.Default.UpgradeRequired)
{
Properties.Settings.Default.Upgrade();
Properties.Settings.Default.UpgradeRequired = false;
Properties.Settings.Default.Save();
}
仅当 UpgradeRequired
标志为 true
时强制升级应用程序设置。对于新版本化的程序集,所有设置都采用默认值,此代码将被触发,并将设置从先前的应用程序版本(如果存在)复制到新版本。
值得注意的是,要使此“技巧”生效,您必须始终在应用程序的第一个版本的应用程序设置中定义此字段。
视图和视图模型
应用程序有几个视图,它们都是 UserControl
并由 MainWindow
承载。这意味着没有弹出对话框!它们是:
- 主
TimerView
SettingsView
AboutView
以及相应的视图模型:
TimerViewModel
SettingsViewModel
AboutViewModel
更改视图和消息传递
由于我们希望使用“关注点分离”或“封装”(如果您愿意),我们不希望视图模型直接通信。为了做到这一点,我们只需使用消息传递,换句话说:
MVVMLight Toolkit 为我们提供了一个单例 Messenger
类,我们可以向其注册消息消费者和消息生产者。因此,要从一个视图模型在一个视图模型中引发一个“事件”,我们只需传递一个消息,例如:
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;
// ....
}
}
}
而在 TimerViewModel
中:
public class TimerViewModel : ViewModelBase
{
private void OnTick(object sender, TimerModelEventArgs e)
{
Messenger.Default.Send(new SimpleMessage(MessageType.TimerTick, TimerValue));
}
}
这样做的好处是:TimerViewModel
更新主窗口 ContentControl
中的 TimerView
倒计时时钟,但我们也希望更新窗口标题以显示倒计时。主窗口 View
绑定到 MainViewModel
,因此为了做到这一点并保持视图模型的分离,我们传递一条包含剩余时间的的消息。稍后会讨论更新窗口标题栏的原因。
任务栏预览和窗口标题
从这个截图可以看出:
倒计时值显示在任务栏项目缩略图和主窗口的标题中。更新窗口标题的原因是,当窗口最小化时,Windows 不会更新任务栏项目缩略图,所以如果您将鼠标指针悬停在任务栏上的图标上,当项目最小化时,缩略图预览将显示窗口最小化时的倒计时。幸运的是,窗口的标题在缩略图预览中会更新,所以我们确保更新它以提供视觉提示给用户。
任务栏消息
我们需要第二种消息类型来在 Windows 7 中通信任务栏进度更新:由于 MainWindow
“视图”绑定到 MainViewModel
,我们需要从 TimerViewModel
接收适合更新任务栏进度指示器的消息。幸运的是,这相对简单,我们再次利用了我们之前看到的 Messenger.Default.Register
和 Messenger.Default.Send
模式。
第二条消息类就是:
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); } }
}
我们的 TimerViewModel
只发送这些消息的实例,而 MainViewModel
接收它们,并通过数据绑定的魔力,在视图模型 (MainViewModel
) 和视图 (MainWindow
) 之间,任务栏进度指示器会自动更新。
<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>
由于 TaskBarItemInfo
缩略图预览提供了更多功能,我们可以添加缩略图“开始”和“暂停”按钮(就像媒体播放器一样),这样我们就可以从缩略图预览控制倒计时器,因此有了上面的 ThumbButtonInfo
元素。
关于 UI 设计的说明
倒计时器 UI 的设计有一定的道理:由于播放和暂停按钮很可能是最常用的,所以它们最大,然后是设置和重置按钮,它们较小,不太可能被意外点击。“关于”窗口通过右下角的一个小“?”来访问。
同样,设置视图中的“确定”和“取消”按钮也分开得较远,以确保您清楚要点击哪个按钮。
最后,除了按钮图标(播放、暂停等)之外,我没有更改应用程序的主题,让操作系统来选择如何主题化它。当然,由于这是一个 MVVM 应用程序,您可以获取源代码,启动 Blend,并按您喜欢的方式进行更改。
甚至还有一些第三方库可以为您完成大量工作,例如 MahApps.Metro。
奖励功能
在文章开头的下载部分,还有一个 MSI 安装程序,供任何想安装计时器而不想深入研究的人使用。
所有源代码都在 github 上,您可以自由地 fork、复制、破解等。MSI 安装程序项目使用 InstallShield,因此它是 github 上托管的解决方案的一部分,但不包含在上面的 zip 文件中(以防您,读者,未安装它)。
最后
如果您觉得这篇文章有帮助或有趣,请投票和/或在下方添加任何评论。谢谢!
修订历史
- 2012 年 2 月 22 日:根据读者评论,对代码库进行了轻微更新。