My Health Assistant 背后的代码奥秘

作者:John Tyrrell

Download PDF

当身体抱恙时,许多人需要服用药品,有时每天需要服用多次。 确保按时服用正确剂量的药品需要人具备非常高的警惕性和纪律性。 软件开发人员 Tim Corey 希望利用技术提供一个从来不会忘记剂量并且具备非常好的记忆力的易用个人“医疗助手” ,以改进自我医疗过程中易出错的情况。 这一想法促使他创建了 My Health Assistant。

My Health Assistant 是一款能够帮助个人通过简单的界面管理和追踪药品使用的应用。 此外,该款应用还可提供健康日记、基于 GPS 的药店和 ER 定位器以及个人健康信息等功能。


图 1: My Health Assistant 的主菜单采用触摸 UI。

Corey 为参加 CodeProject 联合英特尔® 开发人员专区举办的2013 年英特尔® 应用创新竞赛而开发了 My Health Assistant;该款应用转而获得“健康类别”的大奖。 该款应用首次创建是面向 Microsoft Windows* 台式电脑,终极目标是基于 Windows 的平板电脑,如 联想 ThinkPad* 平板电脑 2 运行 Windows 8.1* 的 2 合 1 超极本™、Windows Phone* 8 及其他移动平台。

Corey 的主要开发目标是可移植性、易用性和安全性。 为了实现这些目标,他在开发过程中克服了诸多挑战,包括实施跨平台触摸 UI 并确保敏感医疗数据的安全性。 本案例研究对这些挑战、Corey 采用的解决方案以及他使用的资源进行了探讨。

决策和挑战

Corey 采用了模块化方法构建应用、处理 C# 中的每种功能并使用 XAML Windows Presentation Foundation (WPF) UI 通过 模型-视图-视图模型 (MVVM) 设计模式将其编写到一起。

选择 C#

在创建 My Health Assistant,Corey 在使用面向对象的 .NET 编程语言 C# 方面已积累了丰富的经验。 他选择 C# 作为构建应用的主要语言(使用 Microsoft Visual Studio*)出于诸多原因,包括其拥有微软的支持,这可提供整个生态系统的工具、函数库及其他资源。

C# 作为创建跨平台应用的主要方法,还可用于任何环境:从 PC、Linux* 或 Apple Mac* 到 Apple iPhone* 或 Google Android*。 此外,C# 的优势还包括,安全性和加密已深入地融入代码,将其中任何一种特性移除都非常困难,尤其在处理敏感数据(如医疗记录)时。

Corey 曾考虑过其他的语言,如 VB.NET、其他 .NET 语言以及 Java*, 但是 Corey 认为,这些语言均无法提供 C# 所提供的熟悉度和特性。

C# 函数库

微软针对 C# 提供的生态系统包括大量的优化库,Corey 认为,这些库能够显著简化编码流程。 在 My Health Assistant 中,应用数据存储在 XML 文件中以便支持跨平台移植,而且由于将函数库整合至编程框架的方式,Corey 仅需编写一行简单的代码即可处理所有数据。

除了微软的函数库以外,C# 还可使用多种第三方函数库。 对于 My Health Assistant 的 UI 框架,Corey 使用了 Caliburn.Micro,这能够支持他使用 MVVM 连接应用的前端和后端代码。 这种方法可在编写 UI 时提供极大的灵活性,且在修改后无需再进行复杂的编码。

WPF UI

为了构建 UI,Corey 选择了 Microsoft WPF 系统而未选择 Windows Forms,因为它对屏幕尺寸的变化具备出色的响应能力,这是跨平台开发的一个重要因素。 借助使用 WPF 的 Windows 8 台式电脑和 Windows Phone 8,Corey 为每个平台快速创建了不同版本的应用,并未大量进行 UI 重新编码。

在实际情况下,具备出色相应能力的 WPF UI 根据可用的屏幕大小提供特定数量和尺寸的元素。 全尺寸的台式机视图将会显示全部按钮,而移动视图仅显示 1 或 2 个按钮,并将其他按钮移动至下拉菜单中。

克服触摸和滚动问题

便携设备,无论是智能手机、平板电脑还是超极本设备上的任何应用都需要高效的触摸控制。 虽然台式机应用能够使用鼠标,但是 Corey 特别将其设计为支持触摸功能,以确保能够使用手指来进行简单的操作,如滚动菜单。 他甚至禁用了滚动条以鼓励大家使用手指来滚动屏幕。

Corey 在开发过程中所面临的最大问题是在触摸 UI 中实施菜单滚动。 该应用需要获得准确的屏幕方向以及菜单和其他应用可使用的屏幕空间尺寸;否则,应用就会认为有更多空间可用,从而将主要组件(如菜单按钮)渲染为不可见,因此便无法使用。

为了在 WPF 中支持触摸滚动, Corey 向 ScrollViewer 添加了一个属性,代表 PanningMode,请见以下代码片段。

<ScrollViewer Grid.Row="2" HorizontalScrollBarVisibility="Disabled" 
              VerticalScrollBarVisibility="Hidden" HorizontalAlignment="Center"
              PanningMode="VerticalOnly" Margin="0,0,0,0">

GPS 定位设备

My Health Assistant 的一个主要功能是,无论用户身在何处,它都能够帮助用户找到最近的药店或急诊室。 该功能使用了该设备的 GPS 功能和 Google Maps* API,并结合了通过 API 输入的相关位置数据,可提供精确、相关的地图信息。


图 2: Google Maps* API 集成支持用户轻松定位最近的药店或 ER。

以下代码是包含 GPS 代码的类,该类负责获取坐标并在进行定位后发起事件。 它是异步传输,这意味着,定位到坐标后,应用将继续正常运行。

public class GPSLocator
{
    public GeoCoordinateWatcher _geolocator { get; set; }

    public GPSLocator()
    {
        // Initializes the class when this class is loaded
        _geolocator = new GeoCoordinateWatcher();
    }

    // Asynchronously loads the current location into the private variables and
    // then alerts the user by raising an event
    public void LoadLocation()
    {
        try
        {
            _geolocator = new GeoCoordinateWatcher(GeoPositionAccuracy.Default);
        }
        catch (Exception)
        {
        }
    }
}

以下的代码部分调用了 GPSLocator 类,并可异步提供坐标。 此外,该代码还可提供继续获取新 GPS 坐标的选项,但是在 My Health Assistant 中,Corey 认为用户是固定的,因此将只需要一套坐标。 但是,需要保留 GPS 服务提供持续更新的坐标。

// Initializes the GPS
gps = new GPSLocator();

// Loads the watcher into the public property
gps.LoadLocation();

// Wires up the code that will be fired when the GPS coordinates are found.
// Finding the coordinates takes a couple seconds, so even though this code
// is here, it won't get fired right away. Instead, it will happen at the end
// of the process.
gps._geolocator.PositionChanged += (sensor, changed) =>
{
    // This code uses an internal structure to save the coordinates
    currentPosition = new Position();
    currentPosition.Latitude = changed.Position.Location.Latitude;
    currentPosition.Longitude = changed.Position.Location.Longitude;

    // This notifies my front-end that a couple of my properties have changed
    NotifyOfPropertyChange(() => CurrentLatitude);
    NotifyOfPropertyChange(() => CurrentLongitude);

    // A check is fired here to be sure that the position is correct (not zero).
    // If it is correct, we stop the GPS service (since it will continue to give
    // us new GPS coordinates as it finds them, which isn't what we need). If
    // the latitude or longitude are zero, we keep the GPS service running until
    // we find the correct location.
    if (currentPosition.Latitude != 0 && currentPosition.Longitude != 0)
    {
        gps._geolocator.Stop();
        LoadPharmacies(); 
    }
};

// This is where we actually kick off the locator. If you do not run this line,
// nothing will happen.
gps._geolocator.Start();

API 集成

Corey 知道对于提供本地药店和医院信息,选择合适的 API 至关重要,因为 My Health Assistant 必须能够提供全球各地的准确信息,而不仅是美国。 Corey 对多款具备出色潜力的 API 进行了考虑,包括 Walgreens 和 GoodRx API,但是不得不作罢,因为它们无法在美国以外的国家和地区使用。 Corey 最终选择了包含全球信息的 Factual Global Places API 数据库。 他在西班牙参加一次会议时对其效果进行了测试:当他请求最近的药店时,该应用生成了一个他所在位置数米内的药店列表。


图 3: 用户能够在应用内存储其私人医生、药店和医保信息。

安全性选择

除了可移植性以外,Corey 将安全性作为该款应用的第二大要素。 该款应用的默认设置是本地存储数据,这代表将会带来较低的安全风险。 但是,在测试期间,Corey 发现用户希望在不同的设备上使用应用时访问存储的数据,这需要基于云的数据存储解决方案,从而会增加风险。

对于 XML 数据文件的云备份,Corey 没有在应用中实施复杂的 API 驱动解决方案,而是使用了更为简单的方式,将基于云的保存选项添加至本地选项旁边的资源管理器视图。 这种方法让数据备份更为直观,且支持用户在所选择的服务中实施加密技术和信任程度,以确保数据的安全 — 无论是 Microsoft SkyDrive*、Dropbox*、Box.net 还是其他服务。 以下截图展示了云存储保存选项在资源管理器视图中的显示状态。


图 4: 用户可以在本地备份,或直接备份至其所选的使用资源管理器的云服务。

保存数据

最初,Corey 在处理备份功能时遇到困难,开始为 XML 文件实施复杂的存储和检索机制。 但是,一位朋友为他提供了一个简单但强大的代码,立刻帮他解决了这一问题。

将应用的所有数据都保存至 XML 文件,然后在运行时加载这些文件,以便再次为应用提供这些数据。 以下是实际保存数据的代码。

// This method saves the data to a XML file from a class instance that
// has been passed into this method. It uses generics (the T
// that is all over in here) so that any type can be used to pass
// data into this method to be saved.
public static void SaveData<T>(string filePath, T data)
{
    // Wraps the FileStream call in a using statement in order to ensure that the
    // resources used will be closed properly when they are no longer needed.
    // This file is created in read/write mode.
    using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite))
    {
        // This uses the XmlSerializer to convert our data into XML format
        XmlSerializer xs = new XmlSerializer(typeof(T));
        xs.Serialize(fs, data);
        fs.Close();
    }
}

以下是随后加载数据的代码。

// This method loads the data from a XML file and converts it back into
// an instance of the passed in class or object. It uses generics (the T
// that is all over in here) so that any type can be passed in as the
// type to load.
public static T LoadData<T>(string filePath)
{
    // This is what we are going to return. We initialize it to the default
    // value for T, which as a class is null. This way we can return the
    // output even if the file does not exist and we do not load anything.
    T output = default(T);

    // Checks first to be sure that the file exists. If not, don't do anything.
    if (File.Exists(filePath))
    {
        // Wraps the FileStream call in a using statement in order to ensure that the
        // resources used will be closed properly when they are no longer needed.
        // This opens the file in read-only mode.
        using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
        {
            // This uses the XmlSerializer to convert the XML document back into
            // the class format.
            XmlSerializer xs = new XmlSerializer(typeof(T));
            output = (T)xs.Deserialize(fs);
            fs.Close();
        }
    }

    // Returns the class instance, loaded with the data if the file was found
    return output;
}

代码在应用中的运行方式非常简单。 首先,使用以下命令将类中存储的数据保存至磁盘上的 XML 文件。

_records = Data.LoadData<RecordsPanelViewModel>(filePath);

使用以下命令将数据从 XML 文件加载回类,例如,当应用启动时,需要将数据重新加载回来。

Data.SaveData<RecordsPanelViewModel>(FilePath, this);
Data.SaveData<RecordsPanelViewModel>(FilePath, this);

测试

Corey 在第一开发阶段测试应用时遇到的第一个问题是出现了大量出乎意料的不良输入。 他在不同阶段分别将应用发放给几位朋友和家人,经证明,这能够有效地从用户角度公正地辨别问题,并改善 UI 和总体可用性。

模块化开发方法使得重新安排 UI 流程快速而简单,这使得能够对 Corey 收到的反馈快速迭代。

许多 UI 漏洞导致出现问题,尤其是滚动方面,最初并没有按照 Corey 预想的方式运行。 他修复的另一个漏洞出现在药品剂量计数器上。 例如,对一种药品进行 6 小时的倒计时将会导致后续药品的倒计时从 5 小时 59 分钟开始而非 6 小时。

Corey 表示,实际构建应用的过程更开心,而调试过程则艰难而漫长,但是,他尚未遇到无法解决的问题。

未来计划

当写这篇文章的时候,Corey 正准备发布 2014 年夏季版 My Health Assistant。 首版将面向 Windows 8 台式机,随后将面向 Windows Phone 8,然后是其他移动平台,包括 iOS 和 Android。 发布后,Corey 希望对用户的反馈进行收集,然后使用这些反馈进行迭代并不断改进应用。

Corey 调查的另一个特性是集成药品查找 API,以便为用户提供有关特定药店的药品以及它们在哪里的价格最便宜等信息。 GoodRx 能够在美国境内提供该服务;Corey 正在寻找一个能够在全球范围内适用的解决方案。

结论

知识增长

虽然 Corey 在创建 My Health Assistant 前曾使用过 XAML,但是大多是从较简单的层面。 在应用上使用 XAML 让他显著地提高了 XAML 知识,并学习到以后如何设计和构建更出色的应用。

除了 XAML 之外,Corey 还显著地扩展了 Caliburn.Micro 知识,它是 Rob Eisenberg 为 WPF 和 XAML 创建的框架。 虽然需要在相对较短的时间内学习大量的内容,但是 Corey 认为他获得知识能够帮助他在未连接的框架环境中达成目标。

经验总结

当向他的软件开发学生提供建议时,Corey 强调了良好规划的必要性,他在开发 My Health Assistant 的过程强化了这种开发方法。 经验告诉他,在设计阶段花费更长的时间则意味着在开发和调试阶段花费的时间更少。

设计流程主要是在纸上进行的,Corey 经常会改变主意。 在开发阶段丢弃理念和特性会变得更加困难,这会导致浪费时间。 经证明,初期设计阶段的迭代更为有效。

此外,Corey 还了解到,不经测试便编入应用,并想当然地认为其应该怎么运行是非常危险的。 Corey 发现,在开发过程中,多次出现某些操作(如滚动)无法按照其预想的方式运行,从而导致不得不将代码丢弃。 Corey 建议在开发阶段构建小型应用,以测试关于特定功能和操作的假设。

开发人员介绍

Tim Corey 于 90 年代末开始成为软件开发人员和 IT 专业人员,职位是编程人员兼 IT 主管。 2011 年,Corey 获得 South University 的 IT 和数据库管理学士学位。 后来,他曾担任一家保险集团的主要编程分析师。 目前,他担任 Epicross 咨询公司的主要技术顾问;而且他还拥有自己的公司,致力于帮助企业通过优化现有技术实现更高的 IT 效率。 此外,他还教授软件开发方面的知识。

有用资源

Corey 大量依赖各种外部资源来寻找解决问题的方案。 其中最主要的是 Pluralsight,它可以提供基于注册的视频培训。 当 Corey 学习新课题或提高现有技能时,经常持续观看 3 或 4 个小时的视频。

对于特定问题,Corey 经常会访问 CodeProject 并从收集的大量文章的“提示和技巧”部分,或通过在论坛上提问来寻找答案。 Corey 访问的其他资源还包括 Stack Overflow,他将其当作软件开发人员的 Wikipedia,在这里,几乎所有能想到的问题都得到解答,而且经常被解答了许多次。

Corey 有时候会使用微软文档,而且会经常从 Twitter 上需求支持。 他还会向 Twitter 好友提问,或直接向特定的个人询问,如 Rob Eisenberg,Corey 说当向他求助时总会获益匪浅。

英特尔® 开发人员专区可提供跨平台应用开发工具和操作信息、平台和技术信息、代码示例以及其他专业知识帮助开发人员进行创新和不断取得进步。  加入我们的物联网Android英特尔® RealSense™ 技术Windows 社区,下载工具、访问 dev 套件、与志趣相投的开发人员分享观点以及参与编程马拉松、竞赛、巡展和当地活动。

相关文章:

 

英特尔和 Intel 标识是英特尔在美国和/或其他国家的商标。
*其他的名称和品牌可能是其他所有者的资产。 
英特尔公司 © 2014 版权所有。 所有权保留。