65.9K
CodeProject 正在变化。 阅读更多。
Home

我的健康助手背后的代码秘密

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2014年5月28日

CPOL

12分钟阅读

viewsIcon

23876

我的健康助手是一款应用程序,可帮助用户通过简单的界面管理和跟踪他们的药物使用情况。该应用程序还具有健康日记、基于 GPS 的药房和急诊室定位器以及个人健康信息功能。

许多人需要服用药物,有时一天多次,以帮助他们保持健康。要确保药物按时服用并剂量正确,需要个人保持警惕和自律。软件开发人员 Tim Corey 看到了一种方法,可以通过使用技术提供一个易于使用的个人用药助手来改进容易出错的自我用药跟踪过程,这个助手永远不会忘记剂量,并且拥有完美的记忆力。这个想法促使他创建了“我的健康助手”。

我的健康助手是一款应用程序,可帮助用户通过简单的界面管理和跟踪他们的药物使用情况。该应用程序还具有健康日记、基于 GPS 的药房和急诊室定位器以及个人健康信息功能。

图 1:“我的健康助手”主菜单,显示触摸 UI。

Corey 开发“我的健康助手”是为了参加由 CodeProjectIntel® Developer Zone 合作主办的 Intel® App Innovation Contest 2013。该应用程序随后赢得了健康类奖项。该应用程序最初是为 Microsoft Windows* 桌面构建的,最终目标是 Windows 平板电脑,例如 Lenovo ThinkPad* Tablet 2运行 Windows 8.1* 的 Ultrabook™ 二合一设备、Windows Phone* 8 和其他移动平台。

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

决策与挑战

Corey 采用了模块化方法来构建应用程序,他使用 C# 分别处理每个功能,并使用 XAML Windows Presentation Foundation (WPF) UI 和 Model View ViewModel (MVVM) 设计模式将它们连接起来。

选择 C#

在开发“我的健康助手”之前,Corey 在面向对象 .NET 编程语言 C# 方面拥有丰富的经验。他选择 C# 作为构建应用程序(使用 Microsoft Visual Studio*)的主要语言,原因有很多,包括 Microsoft 的支持,这带来了完整的工具、库和其他资源生态系统。

作为创建跨平台应用程序的关键方法,C# 也可以应用于几乎任何环境:从 PC、Linux* 或 Apple Mac* 到 Apple iPhone* 或 Google Android*。C# 的一个额外优势是安全性与加密已深度集成到代码中,消除了可能成为巨大障碍的因素,尤其是在处理医疗记录等敏感数据时。

Corey 考虑了其他语言选项,例如 .NET 语言 VB.NET,以及 Java*。然而,在 Corey 看来,它们都无法提供 C# 所具有的熟悉度和功能的相同组合。

C# 库

C# 周围的 Microsoft 生态系统包含大量优化库,Corey 认为这些库极大地简化了编码过程。在“我的健康助手”中,应用程序的数据以 XML 文件形式存储,以便于跨平台移植,并且由于库已集成到编程框架中,Corey 能够编写一行简单的代码就能处理所有数据。

除了 Microsoft 库之外,C# 还有许多第三方库。对于“我的健康助手”的 UI 框架,Corey 使用了 Caliburn.Micro,它使他能够使用 MVVM 连接应用程序的前后端代码。这种方法在编辑 UI 时提供了灵活性,消除了在进行任何修改后进行全面重写的需求。

WPF UI

为了构建 UI,Corey 选择 Microsoft WPF 系统而非 Windows Forms,因为其对屏幕尺寸变化的响应能力,这是跨平台开发的关键要素。由于 Windows 8 桌面和 Windows Phone 8 都使用 WPF,Corey 能够快速为每个平台生成不同版本的应用程序,而无需进行大规模的 UI 重写。

实际上,响应式 WPF UI 会根据可用的屏幕空间以特定的数量和大小显示元素。全桌面视图将显示完整的按钮集,而移动视图将仅显示一两个按钮,其余按钮则移至下拉菜单。

克服触摸和滚动问题

任何适用于便携式设备的应用程序,无论是智能手机、平板电脑还是 Ultrabook 设备,都需要有效的触摸控件。虽然桌面应用程序使用鼠标效果很好,但 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 定位器

“我的健康助手”的一项主要功能是能够帮助用户在任何地方找到最近的药房或急诊室。此功能利用设备的 GPS 功能和 Google Maps* API,结合通过 API 拉取的相关位置数据,在地图上显示准确且相关的信息。

图 2:Google Maps* API 集成使用户可以轻松找到他们最近的药房或急诊室。

下面的代码是包含 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 坐标的选项,但在“我的健康助手”的情况下,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 至关重要,因为“我的健康助手”必须能够在世界任何地方提供准确的信息,而不仅仅是美国。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);

测试

在开发早期阶段测试应用程序时,Corey 的第一招就是想出尽可能多的无效输入。他在不同阶段将应用程序交给了一些朋友和家人,这被证明是一种从无偏见的用户的角度识别问题并优化 UI 和整体可用性的有效方法。

模块化开发方法使得重新排列 UI 的过程特别快速直接,能够根据 Corey 收到的反馈进行快速迭代。

一些 UI 错误导致了问题,尤其是滚动方面,起初并未如 Corey 所预期的那样工作。他修复的另一个错误发生在药物剂量计数器上。例如,一种药物的 6 小时倒计时会导致后续药物的倒计时从 5 小时 59 分钟开始,而不是 6 小时。

Corey 将调试过程描述为粗糙且冗长,与实际构建应用程序本身更有成就感的过程形成鲜明对比,但他至今尚未遇到他无法找到解决方案的问题。

后续步骤

在撰写本文时,Corey 的目标是让“我的健康助手”在 2014 年夏季发布。最初将首先推出 Windows 8 桌面版本,然后是 Windows Phone 8,以及包括 iOS 和 Android 在内的其他移动平台。发布后,Corey 希望收集用户反馈,并利用这些反馈随着时间的推移迭代和改进应用程序。

Corey 正在研究的另一个功能是集成药物查找 API,为用户提供有关特定药物的信息以及在哪里可以找到最优惠价格的药物。GoodRx 是一个提供美国境内此类服务的 API 示例,但仍在寻找一种在全球范围内有效的解决方案。

结论

知识增长

虽然 Corey 在“我的健康助手”之前也接触过 XAML,但大多数时候都处于更简单的层面。开发这款应用程序使他能够显著提升 XAML 知识,并学会如何设计和构建更好的未来应用程序。

除了 XAML,Corey 还大大扩展了他对 Caliburn.Micro 的实践知识,这是 Rob Eisenberg 为 WPF 和 XAML 创建的框架。尽管学习曲线相对陡峭,但 Corey 认为他获得的知识对于了解如何在分离的框架环境中实现功能非常有价值。

关键学习

在给他的软件开发学生提供建议时,Corey 强调了良好规划的必要性——这是一种通过他在“我的健康助手”上的工作得到强化的开发方法。这次经历教导他,在设计阶段花费更多时间意味着在开发和调试阶段花费更少时间。

在涉及大量纸面迭代的设计过程中,Corey 经常放弃一些想法。在开发阶段放弃想法和功能会更加困难,导致时间浪费。在初步设计阶段进行迭代被证明效率更高。

Corey 还认识到在技术上对事物工作方式进行假设而不在代码中测试的危险。在开发过程中,Corey 曾多次发现某些事物,例如滚动,其行为方式与他预期的不同,导致不得不放弃代码。Corey 建议在开发阶段构建小型应用程序来测试有关特定功能和行为的假设。

关于开发者

Tim Corey 于 20 世纪 90 年代末开始了他的软件开发人员和 IT 专业人员生涯,担任过程序员和 IT 总监。2011 年,Corey 从 South University 获得 IT 和数据库管理学士学位。随后的职位包括一家保险集团的首席编程分析师。Corey 目前是他的公司 Epicross 咨询公司的首席技术顾问,该公司致力于通过优化现有技术帮助组织提高 IT 效率。他还教授软件开发。

有用的资源

Corey 在寻找问题解决方案方面严重依赖各种外部资源。其中最主要的是 Pluralsight,它提供基于订阅的视频培训。Corey 在学习新主题或提高现有技能时,经常一次观看 3 到 4 小时的视频。

对于具体问题,Corey 经常访问 CodeProject,在其丰富的文章、技巧和窍门部分搜索答案,或在论坛上提问。Corey 的另一个常用资源是 Stack Overflow,他认为它是软件开发人员的维基百科,几乎任何 imaginable 的问题都在那里得到了解答,而且通常不止一次。

Corey 偶尔使用 Microsoft 文档,并且经常转向 Twitter 获取支持。他要么向他的 Twitter 关注者提出问题,要么将查询定向到特定个人,例如 Rob Eisenberg,Corey 称他在被求助时提供了特别大的帮助。

Intel® Developer Zone 提供跨平台应用程序开发工具和操作指南、平台和技术信息、代码示例以及同行专业知识,以帮助开发人员创新并取得成功。加入我们的 物联网AndroidIntel® RealSense™ TechnologyWindows 社区,下载工具、获取开发套件、与志同道合的开发者分享想法,并参与黑客马拉松、竞赛、路演和本地活动。

相关文章

Intel 和 Intel 标志是 Intel Corporation 在美国和/或其他国家/地区的商标。
*其他名称和品牌可能被声明为他人的财产。
版权所有 © 2014。英特尔公司。保留所有权利。

© . All rights reserved.