从 Silverlight 迁移到 AngularJS





0/5 (0投票)
在本篇白皮书中,了解如何将应用程序从 Silverlight 迁移到 HTML5/JavaScript。在文中,Bernardo de Castilho 以一个简单的 Silverlight 应用程序为例,逐步讲解如何将其 ViewModel 和 View 移植到一个 AngularJS 应用程序中。
引言
从 2007 年开始,Silverlight 吸引了众多开发者的原因在于它能够快速、轻松地编写可维护的 Web 应用程序。这很大程度上归功于 MVVM 模式,它将逻辑与表示分离。应用程序的 ViewModel
部分用 C# 或 VB 编写,包含所有逻辑,便于测试和维护。应用程序的 View
部分用 XAML 编写,并使用丰富的支持类和控件库。
当微软决定停止对 Silverlight 的投入时,许多开发者面临一个艰难的决定。他们如何迁移到现代 Web 平台,同时又不放弃 MVVM 模式的所有优点以及在使用 Silverlight 过程中获得的知识?
在 GrapeCity 旗下的 ComponentOne,我们也面临着这个挑战。我们研究了各种替代方案,并得出结论:HTML5 和 AngularJS 等应用程序框架是未来的最佳选择。AngularJS 提供了关键要素:一个用于单页应用程序的稳定框架、一个模板机制以及对 MVVM 的基本支持。唯一缺少的是创建传统 MVVM 应用所需的控件和一些核心类。
我们知道如何编写控件。所以我们决定添加缺失的部分。
结果便是 Wijmo 5,这是我们最新的 HTML5 和 JavaScript 开发控件库。Wijmo 5 是一个真正的 HTML5 解决方案,基于 JavaScript 和 CSS。它包含了 LOB(行内业务)应用程序常用的所有控件,以及 ICollectionView
接口的 JavaScript 实现和一个具体的 CollectionView
类。
与 AngularJS 结合,Wijmo 5 提供了一个绝佳的平台,可以移植现有的 Silverlight 应用程序,也可以比以往更快地创建全新的 Web 应用程序。
Silverlight 应用程序
为了证明这一点,我们决定将一个 Silverlight 应用程序移植到 HTML5。我们选择使用一个由微软在 Wijmo 5 出现之前编写的现有应用程序。该应用程序名为“DataServicesQuickStart”。这是一个简单的应用程序,但它展示了 Silverlight 和 MVVM 应用程序的许多关键功能,包括:
- 将数据加载到 CollectionView 对象中,这些对象可以对数据进行排序、过滤、分组和分页,还可以跟踪当前选中的项。
ICollectionView
接口代表 MVVM 应用程序中的列表。 - 管理分层数据以反映当前选择。该应用程序加载客户列表。当用户选择一个客户时,应用程序会显示该客户下的订单。当用户选择一个订单时,应用程序会显示订单详细信息。这种主从关系是 MVVM 应用程序的另一个常见功能。
- 将数据绑定到控件。
ComboBox
控件提供客户和订单。TextBox
控件提供订单信息,DataGrid
显示订单详细信息。大多数 LOB 应用程序,包括 MVVM,都使用输入控件和数据网格。
“DataServicesQuickStart”应用程序可在网上找到。您可以在这里查看:
http://samples.msdn.microsoft.com/Silverlight/SampleBrowser/ClientBin/DataServicesQuickStart.html
您可以在这里找到源代码:
http://samples.msdn.microsoft.com/Silverlight/SampleBrowser/#/?sref=DataServicesQuickStart
如果您现在不想运行它,以下是该应用程序的外观:
首次运行应用程序时,它会显示一些空的控件。点击“开始”按钮,它会从包含传统 Northwind 数据库的在线 OData 源加载数据。此时,您可以从屏幕右上角的组合框中选择一个客户,然后查看每个订单的数据。从“开始”按钮下方的组合框中选择一个不同的订单,即可查看订单详细信息。
该应用程序非常简单,但它确实说明了许多 LOB 应用程序所需的主要功能。
移植 ViewModel
为了将应用程序移植到 HTML5,我们首先着手处理 ViewModel。在此示例中,MainPage.xaml.cs 文件包含 ViewModel。代码中有趣的部分如下所示(为简洁起见,已省略错误处理代码):
public partial class MainPage : UserControl
{
// Create the context and root binding collection of Customers.
NorthwindEntities context;
DataServiceCollection<Customer> customerBindingCollection;
// Define the URI of the public sample Northwind data service.
Uri serviceUri = new Uri("http://services.odata.org/Northwind/Northwind.svc/");
void startButton_Click(object sender, RoutedEventArgs e)
{
// Instantiate the context based on the data service URI.
context = new NorthwindEntities(serviceUri);
// Instantiate the binding collection.
customerBindingCollection = new DataServiceCollection<Customer>();
// Register a handler for the LoadCompleted event.
customerBindingCollection.LoadCompleted += customerBindingCollection_LoadCompleted;
// Define a LINQ query that returns all customers.
var query = context.Customers;
// Execute the query.
customerBindingCollection.LoadAsync(query);
}
void customerBindingCollection_LoadCompleted(object sender, LoadCompletedEventArgs e)
{
// Get the binding collection that executed the query.
var binding = (DataServiceCollection<Customer>)sender;
// Consume a data feed that contains paged results.
if (binding.Continuation != null)
{
// If there is a continuation token, load the next page of results.
binding.LoadNextPartialSetAsync();
}
else
{
// Bind the collection to the view source.
CollectionViewSource customersViewSource =
(CollectionViewSource)this.Resources["customersViewSource"];
customersViewSource.Source = binding;
}
}
}
代码声明了一个 customerBindingCollection
对象,其中包含客户列表。点击开始按钮时,代码会分批异步加载数据。当最后一个数据批次加载完成后,它会通过 customersViewSource
资源暴露出来。
这将用客户填充组合框。应用程序使用组合框的 SelectionChanged
事件来加载所选客户的订单。
void customersComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// Get the selected Customer.
Customer selectedCustomer = ((ComboBox)sender).SelectedItem as Customer;
if (selectedCustomer != null)
{
// Load the orders for the selected customer
selectedCustomer.Orders.LoadCompleted += Orders_LoadCompleted;
if (selectedCustomer.Orders.Count == 0)
{
// Load the related Orders for the selected Customer,
// if they are not already loaded.
selectedCustomer.Orders.LoadAsync();
}
}
}
代码与前面的块类似。如果所选客户的订单尚未加载,它将调用 LoadAsync
方法来加载它们。
加载订单详细信息的模式也一样。
如果没有 CollectionView
类来存储数据并跟踪选择,那么将此 ViewModel 移植到 JavaScript 将会很困难。这是该代码的 JavaScript 版本:
// URL to Northwind service
var svcUrl = 'http://services.odata.org/Northwind/Northwind.svc';
// create customers, orders, details view
$scope.customers = new wijmo.collections.CollectionView();
$scope.orders = new wijmo.collections.CollectionView();
$scope.details = new wijmo.collections.CollectionView();
// when the current customer changes, get his orders
$scope.customers.currentChanged.addHandler(function () {
$scope.orders.sourceCollection = [];
$scope.details.sourceCollection = [];
var customer = $scope.customers.currentItem;
if (customer) {
loadData(svcUrl, $scope.orders,
'Customers(\'' + customer.CustomerID + '\')/Orders', {
OrderDate: wijmo.DataType.Date,
RequiredDate: wijmo.DataType.Date,
ShippedDate: wijmo.DataType.Date,
Freight: wijmo.DataType.Number
});
}
});
// when the current order changes, get the order details
$scope.orders.currentChanged.addHandler(function () {
$scope.details.sourceCollection = [];
var order = $scope.orders.currentItem;
if (order) {
loadData(svcUrl, $scope.details,
'Orders(' + order.OrderID + ')/Order_Details', {
UnitPrice: wijmo.DataType.Number
});
}
});
$scope
对象是 AngularJS 应用程序的典型特征。它定义了 ViewModel 中可供 View 访问的部分。在本例中,代码将三个 CollectionView 对象添加到作用域:customers、orders 和 order details。
代码没有处理组合框的 SelectionChanged
事件(虽然它可以这样做),而是附加了 CurrentChanged
事件的处理器。这样,任何时候当前客户发生变化(无论是由组合框选择还是通过其他方式),代码都会加载该客户的订单。当前订单更改时,代码会加载所选订单的详细信息。
非常简单,非常 MVVM。
与之前一样,该过程在用户点击开始按钮时启动。
// load the customers when the user clicks the Start button $scope.startButton_Click = function () { loadData(svcUrl, $scope.customers, 'Customers'); }
现在只剩下 loadData
函数了。我们使用 jQuery.ajax 方法来实现它,如果您是 JavaScript 开发者,可能已经多次使用过它。
// utility to load OData into a CollectionView
function loadData(baseUrl, view, table, types) {
// build url
var url = baseUrl + '/' + table;
url += (url.indexOf('?') < 0) ? '?' : '&';
url += '$format=json';
// go get the data
$.ajax({
dataType: 'json',
url: url,
success: function (data) {
// append new items
for (var i = 0; i < data.value.length; i++) {
// convert data types (JSON doesn’t do dates...)
var item = data.value[i];
if (types) {
for (var key in types) {
if (item[key]) {
item[key] = wijmo.changeType(item[key], types[key]);
}
}
}
// add item to collection
view.sourceCollection.push(item);
}
// continue loading more data or refresh and apply to update scope
if (data['odata.nextLink']) {
loadData(baseUrl, view, data['odata.nextLink']);
} else {
view.refresh();
view.moveCurrentToFirst();
$scope.$apply();
}
}
});
}
此方法中有趣的部分是:
- 数据以 JSON 格式返回,而不是 XML。
- 数据类型在必要时会进行转换(因为 JSON 不支持日期)。
- 递归调用会继续加载数据,直到检索到最后一项。
- 数据加载完成后调用 $scope.apply 会通知 AngularJS 更新 View。
这就是整个 ViewModel。
移植 View
在原始应用程序中,MainPage.xaml 文件定义了 View。他们实现了 View 中有趣的部分如下:
<!-- Start button --> <Button Content="Start" Click="startButton_Click" /> <!-- ComboBox to pick customers --> <ComboBox ItemsSource="{Binding Source={StaticResource customersViewSource}}" DisplayMemberPath="CompanyName" SelectionChanged="customersComboBox_SelectionChanged"> </ComboBox> <!-- ComboBox to pick orders --> <ComboBox ItemsSource="{Binding}" DisplayMemberPath="OrderID" SelectionChanged="orderIDComboBox_SelectionChanged"> </ComboBox> <!-- DataGrid to show order details --> <sdk:DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Source={StaticResource detailsViewSource}}"> <sdk:DataGrid.Columns> <sdk:DataGridTextColumn x:Name="productIDColumn" Binding="{Binding Path=ProductID}" Header="Product ID" Width="80*" /> <sdk:DataGridTextColumn x:Name="quantityColumn" Binding="{Binding Path=Quantity}" Header="Quantity" Width="60*" /> <sdk:DataGridTextColumn x:Name="discountColumn" Binding="{Binding Path=Discount}" Header="Discount" Width="60*" /> <sdk:DataGridTextColumn x:Name="unitPriceColumn" Binding="{Binding Path=UnitPrice}" Header="Cost Per Unit" Width="60*" /> </sdk:DataGrid.Columns> </sdk:DataGrid>
有一个 ComboBox 控件绑定到客户列表。其 DisplayMemberPath
属性设置为 CompanyName
,因此组合框列出了客户的公司名称。
在此下方是另一个 ComboBox 控件,绑定到客户的 Orders 集合。DisplayMemberPath
属性设置为 OrderID
,因此组合框列出了订单号。
最后,有一个 DataGrid 绑定到选定订单的详细信息。网格包含一个列定义列表,这些定义指定了网格显示详细信息的哪些部分以及如何格式化数据。
如果没有 AngularJS、所需的控件以及允许您将控件添加到应用程序标记中的 AngularJS 指令,将此视图移植到 HTML 将非常困难。
这是 HTML5 版本的 View。请注意,尽管这是 HTML,但它看起来很像上面的 XAML。
<!-- Start button --> <button ng-click="startButton_Click()"> Start </button> <!-- ComboBox to pick customers --> <wj-combo-box is-editable="false" items-source="customers" display-member-path="CompanyName"> </wj-combo-box> <!-- ComboBox to pick orders --> <wj-combo-box is-editable="false" items-source="orders" display-member-path="OrderID"> </wj-combo-box> <!-- FlexGrid to show order details --> <wj-flex-grid items-source="details"> <wj-flex-grid-column binding="ProductID" header="Product ID" width="80*"> </wj-flex-grid-column> <wj-flex-grid-column binding="Quantity" header="Quantity" width="60*"> </wj-flex-grid-column> <wj-flex-grid-column binding="Discount" header="Discount" width="60*" format="p0"> </wj-flex-grid-column> <wj-flex-grid-column binding="UnitPrice" header="Cost Per Unit" width="60*" format="c2"> </wj-flex-grid-column> </wj-flex-grid>
Wijmo 5 ComboBox 控件具有 itemsSource
和 displayMemberPath
属性,有助于移植视图。
FlexGrid 控件也有一个 itemsSource
属性,并且列具有与 Silverlight 中常见但在 HTML 中缺失的 binding
、header
和 width
属性。width 属性甚至支持星号大小调整。
这是 View 的核心。实际实现还有一些额外的细节。我们使用了 Bootstrap 布局库来创建响应式布局,以便应用程序在平板电脑和手机等小型设备上看起来不错。Bootstrap 易于使用;在大多数情况下,您可以将一些 div 元素添加到页面并设置它们的 class 属性为描述所需布局的值。而且它是免费的……
结果
移植工作只花了几个小时,其中一部分时间花在了研究原始应用程序上。您可以在线看到结果:
http://demos.componentone.com/wijmo/5/Angular/PortingFromSL/PortingFromSL/
如果您现在不想运行它,以下是该应用程序的外观:
该应用程序看起来与原始应用程序非常相似,只是更漂亮一些。但它具有以下重要优势:
- 它可以在台式机、平板电脑和手机上运行。
- 它具有响应式布局,因此如果您调整浏览器大小,内容会自动重排,应用程序仍然可用。
- 它的大小仅为原始应用程序的 15%,因此加载速度更快。
我们希望这个例子能有效地说明 Wijmo 5 如何使将 Silverlight 应用程序迁移到 HTML5 成为一项相对轻松的任务。
请记住,Wijmo 5 的设计目的不仅仅是为了移植 Silverlight 应用程序。它的设计宗旨是将最佳 MVVM 功能引入 HTML5 开发,让您比以往任何时候都更具生产力,并以创纪录的时间交付强大、纯粹的 HTML5 应用程序。您可以从 http://wijmo.com/products/wijmo-5/ 下载 Wijmo 5 的免费试用版。