Windows Azure AppFabric 服务总线

使用可移植类库创建连续客户端

David Kean

下载代码示例

我感到幸运,住在不断连接设备的天。 我爱我能够答复电子邮件使用我的电话,乘公车回家。 它是能够 Skype 和我在世界的另一边的家人和我的 Xbox 上全国各地与志同道合的玩家合作,令人惊叹。 然而,在这个世界的永久的互联网连接,有,约书亚 Topolsky 说,"在我们的计算体验的缺失链接"(engt.co/9GVeKl)。

此缺失的环节是指缺乏 Topolsky 所调用的连续的客户端 ; 就是在今天发生当从一个设备移动到另一个的破碎工作流解决方案。 正如我在我的电脑、 平板电脑和电话之间切换中典型的一天,我当前的浏览会话、 文档、 windows 和应用程序状态应该自然流所有这些。 这样一来,我会花较少的上下文切换时间和更多的时间对实际工作和娱乐。

在本文中,我将展示如何构建一个简单的连续客户端应用程序在跨越多个设备和平台。 我会让使用的新的便携式类库 (PCLs) 以纾缓跨平台应用程序),发展与云 — — 特别是 Windows Azure AppFabric 服务巴士中 — — 来处理设备之间的通信。

在你回家的路 … …

这是下午晚些时候,我在试图快速修复这个最后的 bug,所以我可以避免交通繁忙的工作。 不可避免的电话打来电话:"亲爱的你能回家的路上拾起一些牛奶、 面包和鹰嘴豆吗?"我挂了,去商店,实现忘了买些什么。 最后,我回家与我们已有茶水间的项。 这是令人沮丧,和今天的解决方案往往涉及很多-和往复电话咨询:"你说速冻的豌豆或鹰嘴豆吗?""鹰嘴豆。 和你在那儿,你可以买卫生纸吗?"

为减轻我们的婚姻紧张局势,围绕这个问题 (其他人将不得不等待另一天),我将写一个简单的应用程序称为"对您的方式主页"我们基于 Windows Phone 的设备和测试版 Windows 8 片上运行,并允许我妻子和我轻松地跟踪我们的购物清单。 它会让我们保持两个通知的购物清单所做的任何更改的实时这样在任何时候,我们知道正是我们要买。

鉴于这种情况运行窗口电话和基于 Windows 8 的平板电脑不同的设备,与不同口味的 Microsoft 智能手机。NET 框架和 Windows,我将使用 PCLs 抽象远离平台的差异,并使我分享尽可能多的应用程序逻辑,包括所有的尽可能的与 Windows Azure AppFabric 服务总线的通信。 我还将使用模型-视图-ViewModel (MVVM) 模式 (bit.ly/GW7l),以便使用相同的模型和 ViewModels 从我们的特定于设备的意见。

便携式类库

在过去,在跨平台开发。NET 框架坎坷。 虽然。NET Framework 作为一个跨平台运行时大梦想,微软还没有尚未完全交货的承诺。 如果你已经过尝试传递。基于.NET 框架的应用程序或跨多个设备的框架,您就会注意妨碍了几件事。

在运行库侧保理业务,程序集版本控制和程序集名称是不同之间。网络平台。 例如,System.Net.dll 上的。NET 框架,其中包含对等网络的 Api,意味着在 Silverlight,它是其中包含核心网络堆栈上完全不同的东西。 在上找到这些 Api。NET 框架,您需要参考 System.dll。 程序集版本也不一样 ; 而对于通过 2.0.0.0 和 4.0.0.0 Silverlight 将采用 2.0.5.0 版本 2.0 到 4。NET 框架版本 2.0 到 4。 这些差异,在过去,防止从运行在另一个平台编译的程序集。

从一开始 Visual Studio 侧右边,您需要决定哪一目标平台 — —。NET 框架、 Silverlight 或 Windows Phone。 一旦作出这一决定,它是非常难移到或支持新的平台。 例如,如果您已经目标。NET 框架,靶向。NET 框架和 Silverlight 意味着创建新的项目和要么复制或链接到该项目的现有文件。 如果你幸运的话,你可能已考虑您的应用程序,以一种特定于平台的作品很容易更换。 如果不是 (和这是可能会更高),你会需要 # if 平台机型的每个方法来生成错误直到你有清理生成。

这是新的 PCLs 有助于。 PCLs,可作为免费附加到 Visual Studio 2010 年 (bit.ly/ekNnsN),并内置 Visual Studio 11 beta 版,提供一种面向多个平台使用单个项目的简单方法。 您可以创建新的 PCL,选择您想为目标的框架 (请参见图 1),并开始编写代码。 被子,PCL 工具处理 API 的分歧,所以你看到唯一的类和成员,它们可用,并跨所有的框架的工作你所选筛选智能感知。 然后可以引用生成的程序集,并没有任何变化,所有指定的框架的情况下运行。

Portable Class Library Target Frameworks
图 1 便携式类图书馆目标框架

解决方案布局

组织使用 PCL 一个跨平台应用程序的典型方式是有一个或多个包含共享的组件的便携式项目,有特定于平台的项目,为每个引用这些项目的平台。 我为此应用程序,需要两个 Visual Studio 解决方案 — — 一个在 Visual Studio 2010 (OnYourWayHome.VS2010) 我的 Windows Phone 应用程序,在 Visual Studio 11 (OnYourWayHome.VS11) 包含我的 Windows 地铁样式应用程序中创建一个包含创建。 我需要多个解决方案,因为在写作时,Windows Phone SDK 7.1 工作只有顶部的 Visual Studio 2010,而新的 Windows 8 工具可仅作为 Visual Studio 11 的一部分。 (目前) 没有同时支持单个版本。 不要绝望,虽然 ; 在 Visual Studio 11 中可用的新功能将帮助我在这儿。 我可以打开在早期版本中创建而不必将它们转换为新格式的大多数项目。 这使我有一个单一的 PCL 项目,并从这两种解决方案中引用它。

数字 23 显示我的应用程序的项目布局。 在­WayHome.Core,PCL 项目中,包含模型、 查看模型、 共同事务和平台的抽象。 在­WayHome.ServiceBus,也是一个 PCL 项目包含将谈谈 Windows Azure 的 Api 的便携版本。 这两个项目在 Visual Studio 2010 的解决方案和 Visual Studio 11 之间共享。 OnYourWayHome.Phone 和 OnYourWayHome.Metro 是针对 Windows Phone 7.5 的特定于平台的项目和。分别净地铁样式的应用程序。 这些包含特定于设备的视图 (如应用程序中的页) 和 OnYourWayHome.Core 和 OnYourWayHome.ServiceBus 中找到的抽象的实现。

Windows Phone Project Layout in Visual Studio 2010
图 2 Windows Phone 项目布局在 Visual Studio 2010 年

Windows Metro-Style App Project Layout in Visual Studio 11
图 3 Windows 地铁样式应用程序项目布局在 Visual Studio 中 11

将现有的库转换为 PCLs

与 Windows Azure 进行沟通,我下载了从基于 Silverlight 的其余部分样品 servicebus.codeplex.com 和它转换成 PCL 项目。 某些库是转换比其他人更容易,但你可能会不可避免地遇到一个给定的类型或方法不可用的情况。 这里是一个给定的 API 可能不支持在 PCLs 中的一些典型原因:

API 并不是由传统的所有平台实现的。NET 框架文件 IOs,如 System.IO.File 和 System.IO.Directory,属于此斗。 Silverlight 和 Windows Phone 使用 System.IO.IsolatedStorage Api (虽然不同的。NET 框架版本),而 Windows 8 地铁样式的应用程序使用 Windows.Storage。

API 不兼容跨所有平台一些 Api 的外观和感觉是一样的但它的硬盘或不可能的便携式和一致的方式编写针对他们的代码。 ThreadStaticAttribute,使静态字段,在每个线程的唯一值,是一个例子。 虽然目前的 Windows Phone 和 Xbox 平台上,其运行时都不支持它。

在未来的平台,目前的 API 是被认为已过时或遗产这些 Api 包含不太可能的行为或他们已更换过的较新的技术。 BackgroundWorker 是一个例子 ; 它被取而代之的任务和新的异步程序功能,在 Visual Studio 11 beta 版。

我们用完时间最 Api 而不是编写的铭记的可移植性。 我们花了大量的时间通过每个 API,以确保以可移植的方式对可以编程。 这可能涉及调整或将添加到使便携式 API。 由于的时间和精力参与我们可在 Visual Studio 库的 PCLs 的第一个版本,我们优先高价值、 高使用的 Api。 System.Xml.Linq.dll 和 System.ComponentModel.DataAnnotations.dll 是不是第一版本中可用,但现在都在 Visual Studio 11 beta 版中可用的 Api 的例子。

有几个不同的方式处理掉进其中一种情形的 API。 有时有简单的更换。 例如,有了密切的方法 (Stream.Close、 TextWriter.Close 等) PCL 中不建议使用,并将其替换为处置。 在这种情况下,它只是用后者取代前者的调用。 但有时它有点难,并进行更多的工作。 一种情况下我遇到转换服务总线 Api 时涉及的 HMAC SHA256 哈希代码提供程序。 不是可用的 PCL 因为 Windows Phone 和地铁样式应用程序的加密技术差异。 Windows Phone 应用程序使用。基于网络的 Api 来加密,解密和哈希数据,而地铁样式的应用程序使用新的本机 Windows 运行 (WinRT) 的 Api。

特别是,无法生成后转换是以下代码:

using (HMACSHA256 sha256 = new HMACSHA256(issuerSecretBytes))
{
  byte[] signatureBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(token));
  signature = Convert.ToBase64String(signatureBytes);
}

为了帮助电话加密 Api 和 WinRT 加密 Api 之间的差距,我发明了一个平台抽象代表服务总线的要求。 在此情况下,服务总线,需要一种计算 HMAC SHA256 哈希值:

public abstract class ServiceBusAdapter
{
  public static ServiceBusAdapter Current
  {
    get;
    set;
  }
  public abstract byte[] ComputeHmacSha256(byte[] secretKey, byte[] data);
}

我添加 ServiceBusAdapter 便携式的项目,以及用于设置的当前的抽象,稍后将成为重要的静态属性。 接下来,我创建了 Windows Phone 和 Windows 8 特定 HMAC SHA256 实现的这一抽象,并把这些按各自的项目,如中所示图 4

图 4 HMAC SHA256 实现为 Windows Phone 和 Windows 8

// Windows Phone implementation
public class PhoneServiceBusAdapter : ServiceBusAdapter
{
  public override byte[] ComputeHmacSha256(byte[] secretKey, byte[] data)
  {
    using (var cryptoProvider = new HMACSHA256(secretKey))
    {
      return cryptoProvider.ComputeHash(data);
    }
  }
}
// Windows 8 implementation
public class MetroServiceBusAdapter : ServiceBusAdapter
{
  private const string HmacSha256AlgorithmName = "HMAC_SHA256";
  public override byte[] ComputeHmacSha256(byte[] secretKey, byte[] data)
  {
    var provider = MacAlgorithmProvider.OpenAlgorithm(HmacSha256AlgorithmName);
    var key = provider.CreateKey(_secretKey.AsBuffer());
    var hashed = CryptographicEngine.Sign(key, buffer.AsBuffer());
    return hashed.ToArray();
  }
}

时才在 Windows Phone 项目启动时,我然后"引导"服务总线通过作为当前适配器设置的电话特定适配器:

ServiceBusAdapter.Current = new PhoneServiceBusAdapter();

我也是这样一种 Windows 8 的项目:

ServiceBusAdapter.Current = new MetroServiceBusAdapter();

在地方的一切,我然后更改原始非编译的代码,以通过适配器调用:

var adapter = ServiceBusAdapter.Current;
byte[] signatureBytes = adapter.ComputeHmacSha256(issuerSecretBytes, Encoding.UTF8.GetBytes(token));

所以虽然有两种不同方式的计算哈希值取决于平台,便携式项目会谈都使用一个单一的界面。 这可能需要一点点的工作,但我可以方便地重用基础设施,如我跑进了更多的 Api,需要平台之间的桥接。

请注意,我使用一个静态属性访问并注册的适配器,使现有的 Api 移到使用适配器容易。 如果您使用的一个依赖项注射框架如管理可扩展性框架 (城域)、 统一或 Autofac,你会发现这是天然的平台特定适配器注册到容器并"注入"适配器需要它的便携式组件的容器。

应用版式

我购物列表应用,对你路回家,有两个简单的视图:ShoppingListView,其中显示当前项目的清单 ; 和 AddGroceryItemView,它允许用户在列表中添加更多物品。 数字 56 的 Windows Phone 版本,这些视图的显示。

ShoppingListView
图 5 ShoppingListView

AddGroceryItemView
图 6 AddGroceryItemView

ShoppingListView 显示所有项目,仍未购买,当你漫步在商店附近,您检查关闭每个项目,将其添加到购物车的想法。 后购买项目,单击签出导致采取关闭的列表中,指示他们不再需要购买的选中的项。 即时共享相同的购物清单的设备 (好吧,因为它背后的网络允许的即时) 看到另一个人所做的更改。

住在特定于平台的项目的意见主要包含 XAML 和有很少代码隐藏中限制了您需要两个平台之间复制的代码的数量。 视图使用 XAML 数据绑定,绑定自己提供的命令和运行意见的数据的便携式 ViewModels。 因为有没有船舶在所有平台的常见的 UI 框架,PCL 项目不能引用 UI 特定的 Api。 然而,针对框架,支持他们,他们可以利用通常由 ViewModels 使用的 Api。 这包括使 XAML 数据绑定的工作,如 INotifyPropertyChanged、 ICommand 和 INotifyCollectionChanged 的核心类型。 此外,虽然 WinRT XAML 框架不支持他们,添加了 System.ComponentModel.DataAnnotations 和 INotifyDataErrorInfo 的完整性,和这使自定义 XAML 验证框架,支持便携式 ViewModels/模型。

数字 78 显示视图/ViewModel 相互作用的示例。 图 7 显示窗口电话版本 AddGroceryItemView 和其绑定的控件。 这些控件绑定属性对上的 AddGroceryItemViewModel,如中所示的 Windows Phone 和 Windows 8 的项目,与共享图 8

图 7 AddGroceryItemView 控件窗口电话

<StackPanel>
  <TextBox Text="{Binding Name, Mode=TwoWay}"
           Width="459"
           Height="80" />
  <Button Command="{Binding Add}"
          Content="add"
          Margin="307,5,0,0" />
  <TextBlock Text="{Binding NotificationText}"
             Margin="12,5,0,0"/>
</StackPanel>

图 8 为 Windows 8 的的 AddGroceryItemViewModel 类

public class AddGroceryItemViewModel : NavigatableViewModel
{
  private string _name;
  private string _notificationText;
  private ICommand _addCommand;
  [...]
  public ICommand AddCommand
  {
    get { return _addCommand ??
(_addCommand = new ActionCommand(Add)); }
  }
  public string Name
  {
    get { return _name ??
String.Empty; }
    set { base.SetProperty(ref _name, value, "Name"); }
  }
  public string NotificationText
  {
    get { return _notificationText ??
string.Empty; }
    set { base.SetProperty(ref _notificationText, value, "NotificationText"); }
  }
}

事件采购

你的方式家基于大量围绕事件采购的概念 (bit.ly/3SpC9h)。 这个主意是发布到应用程序的所有状态更改,存储为一系列的事件。 在这方面,事件不是指由 C# 事件关键字定义的 (虽然这个想法是相同) 的事,而是要具体的类表示单个更改到系统。 这些都通过了所谓的事件的集合,然后通知做工作来响应事件的一个或多个处理程序发布。 (有关事件聚合的更多信息,参阅肖恩 · 维尔德穆特文章,"复合 Web 应用程序与棱镜,"在 msdn.microsoft.com/magazine/dd943055。)

例如,该事件表示杂货项添加到购物列表看起来类似所示图 9

图 9 项添加的事件

// Published when a grocery item is added to a shopping list
[DataContract]
public class ItemAddedEvent : IEvent
{
  public ItemAddedEvent()
  {
  }
  [DataMember]
  public Guid Id
  {
    get;
    set;
  }
  [DataMember]
  public string Name
  {
    get;
    set;
  }
}

ItemAddedEvent 类包含有关事件的信息:在此情况下,添加了的杂货项和已用于唯一地表示内购物杂货项的 ID 的名称列表。 事件也标有 [DataContract],这使得他们要序列化到磁盘或通过网络发送更容易。

此事件是创建和发布当用户单击添加按钮上 AddGroceryItemView,如图所示,在图 10

图 10 发布事件

public class AddGroceryItemViewModel : NavigatableViewModel
{
  private readonly IEventAggregator _eventAggregator;
  [...]
  // Adds an item to the shopping list
  private void Add()
  {
    var e = new ItemAddedEvent();
    e.Id = Guid.NewGuid();
    e.Name = Name;
    _eventAggregator.Publish(e);
    NotificationText = String.Format("{0} was added to the shopping list.", Name);
    Name = string.Empty;
  }
}

请注意此方法不会直接进行任何更改了购物清单 ; 它只是发布的 ItemAddedEvent 事件聚合器。 它是一个事件处理程序听此事件做些事情的责任。 在此情况下,一类称为 ShoppingList 赞同并处理事件,如图所示,在图 11

图 11 ShoppingList 类

public class ShoppingList : IEventHandler<ItemAddedEvent>                                        
{
  public ShoppingList(IEventAggregator eventAggregator)
  {
    Requires.NotNull(eventAggregator, "eventAggregator");
    _eventAggregator = eventAggregator;
    _eventAggregator.Subscribe<ItemAddedEvent>(this);
  }
  [...]
  public ReadOnlyObservableCollection<GroceryItem> GroceryItems
  {
    get { return _groceryItems; }
  }
  public void Handle(ItemAddedEvent e)
  {
    var item = new GroceryItem();
    item.Id = e.Id;
    item.Name = e.Name;
    item.IsInCart = false;
    _groceryItems.Add(item);
  }
}
}

每次发布 ItemAddedEvent,ShoppingList 创建新的 GroceryItem,使用从事件数据,并将其添加到购物清单。 ShoppingListView,而间接地绑定到同一列表中通过其 ShoppingListViewModel,也会更新。 这意味着当用户导航到购物列表页面,他刚添加到列表中的项目都显示为预期。 从购物列表中移除项的过程中,将项目添加到购物车和签出该购物车是所有处理使用相同的事件发布订阅模式。

它可能最初看起来像很多间接寻址的简单地将项添加到购物清单:AddGroceryItemViewModel.Add 方法发布到 IEventAggregator,而将其传递到的 ShoppingList,将其添加到购物清单上的事件。 为什么不会 AddGroceryItemViewModel.Add 方法只是绕过 IEventAggregator 和 ShoppingList 中直接添加新的 GroceryItem? 我很高兴你问。 作为事件的系统治疗所有状态更改的优点是它鼓励非常松散耦合应用程序的所有单个的部分。 因为在发布服务器和订阅服务器不知道彼此,管道中插入一个新的功能如同步数据,并从云,是简单得多。。

同步数据到云

我已经介绍,在单个设备上运行的应用程序的基本功能,但仍有问题的获取用户购物列表,其他设备,以使所做的更改,反之亦然。 这是 Windows Azure AppFabric 服务总线是哪里来。

Windows Azure AppFabric 服务总线是一种功能,可以轻松地谈得来,在互联网上,避免复杂的导航信息沟通障碍如防火墙和网络地址转换 (NAT) 设备的应用程序和服务。 它提供了 Windows Azure 主办的休息和 Windows 通讯基础 (WCF) HTTP 端点,并坐在发布服务器和订阅服务器之间。

有三种主要的方法,使用 Windows Azure AppFabric 服务总线 ; 通信 然而,我的应用程序而言,我就将只涵盖的主题。 全面概述,签出"介绍到 Windows Azure AppFabric 服务总线"在 bit.ly/uNVaXG

对于出版商,服务总线主题是类似于在云大队列 (请参阅图 12)。 完全不知道谁正在侦听,出版商推送消息的主题,在他们被拘留无限直到订阅服务器所要求的。 要获取消息从队列,订户拉从发布到相应主题的邮件筛选的订阅。 订阅行为像特定队列,并仍从预订中移除的消息将会看到从其他订阅中,如果他们自己的过滤器,包括它们。

Service Bus Topic
图 12 服务总线主题

在对你的方式家,AzureServiceEventHandler 类是应用程序和服务总线之间的桥梁。 类似于 ShoppingList,它还实现 IEventHandler <T>,但的特定的事件,而不是 AzureServiceEventHandlers 可以处理它们所有,如图所示,在图 13

The AzureServiceEventHandler Class
图 13 AzureServiceEventHandler 类

public class AzureServiceBusEventHandler : DisposableObject, IEventHandler<IEvent>, IStartupService
{
  private readonly IAzureServiceBus _serviceBus;
  private readonly IAzureEventSerializer _eventSerializer;
  public AzureServiceBusEventHandler(IEventAggregator eventAggregator,
    IAzureServiceBus serviceBus, IAzureEventSerializer eventSerializer)
  {
    _eventAggregator = eventAggregator;
    _eventAggregator.SubscribeAll(this);
    _serviceBus = serviceBus;
    _serviceBus.MessageReceived += OnMessageReceived;
    _eventSerializer = eventSerializer;
  }
  [...]
  public void Handle(IEvent e)
  {
    BrokeredMessage message = _eventSerializer.Serialize(e);
    _serviceBus.Send(message);
  }
}

每个用户进行购物清单的状态的更改是由 AzureServiceBusEventHandler 处理,直接推向云。 既不是 AddGroceryItemViewModel,出版事件,也不是 ShoppingList,负责处理它在本地设备上,是意识到这种情况发生。

从云回旅行是其中一种基于事件的体系结构是值得的。 AzureServiceEventHandler 检测 (通过 IAzureServiceBus.MessageReceived C# 事件) 的服务总线上已收到新邮件时,它不会的较早前做反向,回事件,将反序列化所收到的邮件。 从这里,它获取发布回通过事件聚合器,使它被视为好像事件是来自在应用程序中,如中所示图 14

图 14 反序列化收到的邮件

public class AzureServiceBusEventHandler : DisposableObject, IEventHandler<IEvent>,
  IStartupService
{
  private readonly IAzureServiceBus _serviceBus;
  private readonly IAzureEventSerializer _eventSerializer;
  [...]
  private void OnMessageReceived(object sender, MessageReceivedEventArgs args)
  {
    IEvent e = _eventSerializer.Deserialize(args.Message);
    _eventAggregator.Publish(e);
  }
}

ShoppingList 不知道 (也不关心的它) 有关的事件和处理那些来自服务总线云,仿佛他们直接来自用户的输入源。 它更新其列表的食品,并从而引起任何更新以及绑定到该列表的视图。

如果你特别注意,您可能会注意在工作流的一个小问题:从本地设备获取发送到云计算的事件回到该同一设备,并导致数据重复。 更糟的是,其他、 无关的购物清单的更改也会对该设备。 不了解你,但我敢肯定我不想看到其他人的食物选择出现在我的购物清单。 要防止出现这种情况,每个列表和每个设备,进行侦听的主题订阅创建服务总线主题。 当从设备情况下,消息发布到主题时,连同订阅筛选器使用排除来自其自身设备的消息的消息,发送包含设备 ID 属性。 图 15 显示此工作流。

Device-to-Device Workflow
图 15 设备到设备工作流

接近尾声了

在这篇文章,我很多涵盖:便携式类库简化我的解决方案,大大减少需要写入目标两个平台的代码的数量。 同时,更改应用程序状态通过事件作出很容易同步云计算该状态。 仍有很多,我已经向左愚钝,不过,你要时发展持续的客户端中的因子。 我没有谈到脱机事件缓存和容错能力 (如果网络不可用时我发布的事件?),合并冲突 (如果另一个用户使与煤矿冲突的更改?),播放 (如果我将新设备附加到购物清单,如何不会它获取更新?),访问的控制 (我如何防止未经授权的用户访问的数据,他们不应该?),最后持久性。 在文章的示例代码中,应用程序不会保存发射之间的购物清单。 我将把这个作为练习你 ; 如果你想玩代码,它可能是一个有趣的挑战。 天真 (或相当传统) 即将来临的持久性的方式付诸表决可直接进入 ShoppingList 类钩、 标记为可序列化的 GroceryItem 对象和关闭将它们保存到一个文件。 前走这条路线,不过,停止,并认为这件事:ShoppingList 已经以本机方式处理的事件,已经不在乎他们从哪里来,同步数据,并从云像竟然保存和恢复数据从磁盘,不是吗?

David Kean 是一个开发者上。在 Microsoft,他在基类库 (BCL) 团队工作网框架团队。在这之前,他从事经常魅力,但也极大地被误解的 FxCop 工具和其相关的一个同级 Visual Studio 代码分析。最初从墨尔本,澳大利亚,他现在设在西雅图、 华盛顿州,与他的妻子露西和三个孩子,杰克、 冬和本。他可以上找到博客 davesbox.com

多亏了以下的技术专家审查这篇文章:Nicholas BlumhardtImmo Landwerth