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

从 Silverlight 迁移到 AngularJS

2014年11月13日

CPOL

7分钟阅读

viewsIcon

36553

在本篇白皮书中,了解如何将应用程序从 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 控件具有 itemsSourcedisplayMemberPath 属性,有助于移植视图。

FlexGrid 控件也有一个 itemsSource 属性,并且列具有与 Silverlight 中常见但在 HTML 中缺失的 bindingheaderwidth 属性。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 的免费试用版。

© . All rights reserved.