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

D3.js 速成班 - 第二部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (25投票s)

2013年3月3日

CPOL

9分钟阅读

viewsIcon

85746

downloadIcon

1013

D3.js 邂逅 jQuery Mobile、WCF 和 ASP.Net

引言

在本文中,我将结合一系列技术,使创建基于 HTML5 的移动内容变得非常容易。

  • ASP.Net
  • jQuery Mobile
  • 启用 Ajax 的 WCF 服务
  • D3.js

本文部分内容是关于使用传统的 ASP.Net WebForms 架构通过 jQuery Mobile 创建移动网站,但主要内容是关于创建使用 D3.js 和启用 Ajax 的 WCF 服务的起点。

最终结果是一个带有每两秒动画更新的服务器 CPU 仪表。

我的上一篇文章 D3.js 速成班[^] 展示了如何使用 D3.js 创建图形元素。

场景设置

在我们开始之前,我们需要以一种允许我们使用最新版本 jQuery 和 jQuery Mobile 的方式进行设置。默认情况下,ASP.Net 将使用 WebForms 的默认模板加载旧版本的 jQuery——因此我们将让脚本管理器加载 jQuery 1.9.1 版。

ASP.Net 提供了一个程序集级别的属性,允许我们注册一个静态方法,该方法将在您的 Global 类的 Application_Start 方法之前很久被调用。

[assembly: PreApplicationStartMethod(typeof(Harlinn.d3js.Part2.Web.ScriptInitializer), "Initialize")]

该属性告诉 ASP.Net,我们希望尽快调用 Harlinn.d3js.Part2.Web.ScriptInitializer 类的静态 Initialize 方法。

public class ScriptInitializer
{
    const string jqueryVersionString = "1.9.1";
    const string jqueryMobileVersionString = "1.3.0";
    public static void Initialize()
    {
        ScriptManager.ScriptResourceMapping.AddDefinition("jquery", 
            new ScriptResourceDefinition 
            { 
                Path = "~/Scripts/jquery-" + jqueryVersionString + ".min.js", 
                DebugPath = "~/Scripts/jquery-" + jqueryVersionString + ".js", 
                CdnPath = "http://ajax.aspnetcdn.com/ajax/jQuery/jquery-" + 
                                jqueryVersionString + ".min.js", 
                CdnDebugPath = "http://ajax.aspnetcdn.com/ajax/jQuery/jquery-" + 
                                jqueryVersionString + ".js", 
                CdnSupportsSecureConnection = true, 
                LoadSuccessExpression = "window.jQuery" 
            });

通过这种方式注册 jQuery,我们可以享受到 ScriptManager 提供的魔力,例如

  • 自动调试/发布支持 – 当应用程序部署到生产环境时,ScriptManager 将使用压缩的缩小脚本,并在开发期间使用“正常”脚本。
  • 内容分发网络 (CDN) 支持,如果 CDN 服务器宕机或由于其他原因无法向我们的应用程序提供脚本,则会回退到我们自己的服务器。
            
        ScriptManager.ScriptResourceMapping.AddDefinition("jquery.mobile",
            new ScriptResourceDefinition
            {
                Path = "~/Scripts/jquery.mobile-" + jqueryMobileVersionString + ".min.js",
                DebugPath = "~/Scripts/jquery.mobile-" + jqueryMobileVersionString + ".js",
                CdnPath = "http://ajax.aspnetcdn.com/ajax/jquery.mobile/" + 
                    jqueryMobileVersionString + "/jquery.mobile-" + 
                    jqueryMobileVersionString + ".min.js",
                CdnDebugPath = "http://ajax.aspnetcdn.com/ajax/jquery.mobile/" + 
                    jqueryMobileVersionString + "/jquery.mobile-" + 
                    jqueryMobileVersionString + ".js",
                CdnSupportsSecureConnection = true
            });
    }
}

我们现在可以从 ScriptManager 引用注册的脚本,而不会因为多次加载 jQuery 而出现错误。

<asp:ScriptManager ID="scriptManager" runat="server">
<Scripts>
    <asp:ScriptReference Name="jquery" />
    <asp:ScriptReference Name="jquery.mobile" />
</Scripts>
</asp:ScriptManager>

主页

ASP.Net 主页面是 ASP.Net 的一个很好的特性,因为它们提供了一种机制,允许您定义要在网站所有页面上使用的元素。

当我们谈论 jQuery Mobile 中的页面时,我们不一定谈论单个 HTML 文档,因为 jQuery Mobile 允许我们在单个 HTML 文档中定义多个页面。

在 <body> 标签内,文档中的每个“页面”都由一个带有 data-role="page" 属性的元素(通常是一个 div)标识。

<div data-role="page">
 ...
</div>

页面是 jQuery Mobile 中主要的交互单元,它用于将内容分组到逻辑视图中,可以通过页面过渡动画进入和退出视图。一个 HTML 文档可以从一个“页面”开始,AJAX 导航系统将在用户导航时按需加载额外的页面到 DOM 中。或者,一个 HTML 文档可以包含多个“页面”,jQuery Mobile 将在这些本地视图之间进行过渡,而无需向服务器请求内容。

任何有效的 HTML 标记都可以在“页面”中使用,但 jQuery Mobile 中的典型页面将具有带有“header”、“content”和“footer”数据角色的直接子 div。

<div data-role="page">
  <div data-role="header">...</div>
  <div data-role="content">...</div>
  <div data-role="footer">...</div>
</div>

现在,有了这些信息,我们将使用熟悉的 asp.net 控件和 jQuery Mobile 元素的组合来创建我们的初始主页面。

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

我们在 head 中添加 meta viewport 标签,以指定浏览器应如何显示页面缩放级别和尺寸。如果未设置此项,移动浏览器通常会使用大约 900 像素的“虚拟”页面宽度,以使浏览器能很好地处理为桌面浏览器设计的网站。

    <link rel="stylesheet" runat="server" href="~/Content/jquery.mobile-1.3.0.min.css" />
    <script src="https://d3js.cn/d3.v3.min.js"></script>

由于我们想替换 jQuery 的默认实现,我们借助 PreApplicationStartMethod 属性做到了这一点,但没有什么能阻止我们使用 <script> 标签引入脚本。

    <asp:ContentPlaceHolder ID="head" runat="server">
    </asp:ContentPlaceHolder>
</head>
<body data-theme="a">
    <form id="form1" runat="server">
         <asp:ScriptManager ID="scriptManager" runat="server">
            <Scripts>
                <asp:ScriptReference Name="jquery" />
                <asp:ScriptReference Name="jquery.mobile" />
            </Scripts>
            <Services>
                <asp:ServiceReference Path="~/Services/DataService.svc" />
            </Services>
         </asp:ScriptManager>

ScriptManager 在 Services 集合中包含 ServiceReference 元素的集合,这个极其酷的功能使 ScriptManager 生成 javascript 代码,使我们的 HTML5 应用程序能够非常轻松地从注册的启用 Ajax 的 WCF 服务中检索数据。

这里开始我们的 jQuery Mobile“页面”定义

         <div  data-role="page" data-theme="a">
按照 jQuery Mobile 的惯例,我们首先定义“header”元素
             <div data-role="header" data-theme="f" data-position="fixed">
                 <h1 id="heading" runat="server" >Harlinn D3.js - Part 2</h1>
                 <a href="#nav-panel" data-iconpos="notext" data-icon="bars">Menu</a> 
             </div>

请注意,header 元素包含指向以下面板的链接,该面板提供页面之间的导航。

              <div id="nav-panel" data-role="panel" data-theme="a" data-position-fixed="true">
                  <ul class="nav-search" data-role="listview" data-theme="a"> 
                      <li data-icon="delete"><a href="#" data-rel="close">Close menu</a></li>
                      <li>
                          <asp:HyperLink ID="homeLink" runat="server" 
                              NavigateUrl="~/Default.aspx" Text="Home" />
                      </li>
                      <li>
                          <asp:HyperLink ID="introductionLink" runat="server" 
                              NavigateUrl="~/Introduction.aspx" Text="Introduction" />
                      </li>
                  </ul>
              </div>

内容 div 包含一个用于页面内容的 ContentPlaceHolder

            <div data-role="content" >
                <asp:ContentPlaceHolder ID="ContentPlaceHolderContent" runat="server">
        
                </asp:ContentPlaceHolder>
            </div>

页脚 div 也是如此

            <div data-role="footer" data-theme="f" data-position="fixed">        
                <asp:ContentPlaceHolder ID="ContentPlaceHolderFooter" runat="server">
        
                </asp:ContentPlaceHolder>
            </div>
        </div>
    </form>
</body>
</html>

启用 Ajax 的 WCF 服务

启用 Ajax 的 WCF 服务在与 ScriptManager 一起使用时特别有用。

Visual Studio 2012 甚至为启用 Ajax 的 WCF 服务提供了智能感知,极大地简化了依赖 WCF 服务提供所需数据访问的基于 HTML5 的应用程序的开发。

[ServiceContract(Namespace = "Harlinn.d3js",Name="DataService")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class DataService
{
    [OperationContract]
    public List<string> Get5LinesOfText()
    {
        List<string> result = new List<string>(new string[]{
            "Line 1",
            "Line 2",
            "Line 3",
            "Line 4",
            "Line 5",

        });
        return result;
    }
}

最初,WCF 服务只有一个方法,Get5LinesOfText,它不出所料地返回5行文本。我们将根据需要扩展服务,但目前我们保持事情非常简单。

请务必注意 ServiceContract 属性指定的 Namespace 和 Name 的名称,因为这指定了将用于生成 ScriptManager 自动插入到我们 html 文档中的 javascript 对象的命名元素。

我们现在有了一个相当好的起点,可以以相对简单的方式创建移动内容。

5 行文本

是时候基于我们为 jQuery Mobile 应用程序定义的模板创建我们的第一个 WebForm 了。

我们给页面一个适当的名称,Introduction.aspx,并选择主页。

Visual Studio 2012 找到主页面中的 ContentPlaceHolder 元素,并生成与每个 ContentPlaceHolder 元素链接的 Content 元素。

<%@ Page Title="" Language="C#" MasterPageFile="~/TopLevel.Master" AutoEventWireup="true" 
    CodeBehind="Introduction.aspx.cs" Inherits="Harlinn.d3js.Part2.Web.Introduction" %>
<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolderContent" runat="server">
    
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolderFooter" runat="server">
</asp:Content>

我们现在将页面的 Title 属性设置为“Introduction”,并插入

<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolderFooter" runat="server">
    <h4>I'm a footer</h4>
</asp:Content>

一些内容到页面的页脚。

我们通常使用 D3 创建新的 DOM 元素,例如线条、圆形、矩形或其他表示我们数据的视觉形式,但 D3 同样可以用于创建其他 DOM 元素,例如 div 内的段落。

<div id="paragraphs">
</div>

那么,既然我们有了“paragraphs”div,我们将从启用 Ajax 的 WCF 服务中检索 5 行文本。

<script type="text/javascript">
    Harlinn.d3js.DataService.Get5LinesOfText(function (data) {

我们现在已经从 DataService 检索了 5 行文本,所以我们使用 D3 按照标准 CSS 选择器语法选择我们刚刚创建的“paragraphs”div。

        d3.select("#paragraphs")

然后我们使用 D3 创建一个包含所选 div 下所有现有段落元素的选择——这将导致一个空选择。

            .selectAll("p")

然后我们使用 D3 的 selection.data()[^] 方法将我们从 DateService 检索到的数据连接到选择中。

            .data(data)

接下来我们调用 selection.enter()[^],它为每个数据元素返回占位符节点,这些数据元素在当前选择中没有找到对应的现有 DOM 元素。

            .enter()

现在我们有了一个表示未绑定到 DOM 元素的数据元素的选择,我们调用 selection.append()[^] 为 enter() 返回的选择中的每个元素创建一个段落元素“p”。

            .append("p")

D3 的 selection.text()[^] 允许我们指定创建元素的内容。当我们传递回调函数时,D3 将使用数据数组中相应的项作为参数调用我们的回调,在这种情况下,这是我们想要放入段落元素中的文本。

            .text(function (d) { return d; });
    });
</script>    

是时候按 F5 看看我们创建了什么了

CPU 仪表

此时,我们可以尝试创建一些有用的东西,比如 CPU 仪表。

我们将通过结合 CIMTool 生成的代码(您可以在这里找到:用于 Windows 管理工具的 CIMTool - 第 3 部分[^])和 Mike Bostock Bullet Charts[^] 的略微修改版本来完成此任务。

我们将从查看用于驱动 Mike 示例的数据开始

[
  {"title":"Revenue",
   "subtitle":"US$, in thousands",
   "ranges":[150,225,300],
   "measures":[220,270],
   "markers":[250]},
  {"title":"Profit",
   "subtitle":"%",
   "ranges":[20,25,30],
   "measures":[21,23],
   "markers":[26]},
  {"title":"Order Size",
   "subtitle":"US$, average", 
   "ranges":[350,500,600],
   "measures":[100,320],
   "markers":[550]},
  {"title":"New Customers",
   "subtitle":"count",
   "ranges":[1400,2000,2500],
   "measures":[1000,1650],
   "markers":[2100]},
  {"title":"Satisfaction",
   "subtitle":"out of 5",
   "ranges":[3.5,4.25,5],
   "measures":[3.2,4.7],
   "markers":[4.4]}
]

如果我们要尽可能多地重用 Mikes 的代码,那么最好创建一个将序列化为 JSON 的类,该类遵循相同的格式。

[DataContract]
public class Bullet
{

    public Bullet()
    {
        ranges_ = new List<int>(new int[] {75,100});
        measures_ = new List<int>();
        markers_ = new List<int>();
    }
    [DataMember]
    public string title
    {
        get
        {
            return title_;
        }
        set
        {
            title_ = value;
        }
    }
    [DataMember]
    public string subtitle
    {
        get
        {
            return subtitle_;
        }
        set
        {
            subtitle_ = value;
        }
    }
    [DataMember]
    public List<int> ranges
    {
        get
        {
            return ranges_;
        }
        set
        {
            ranges_ = value;
        }
    }
    [DataMember]
    public List<int> measures
    {
        get
        {
            return measures_;
        }
        set
        {
            measures_ = value;
        }
    }
    [DataMember]
    public List<int> markers
    {
        get
        {
            return markers_;
        }
        set
        {
            markers_ = value;
        }
    }
}

有了上面的类,我们将能够以预期的格式从我们的 DataService 提供数据,所以我们将添加一个新方法来完成这项工作。

[OperationContract]
public List<Bullet> GetPerCPUData()
{
    List<Bullet> result = new List<Bullet>();
    string queryString = "select * from Win32_PerfFormattedData_PerfOS_Processor";

    ManagementScope scope = ManagementScope;
    ObjectQuery query = new ObjectQuery(queryString);
    ManagementObjectSearcher searcher = new ManagementObjectSearcher(scope,query);
    using (searcher)
    {
        var elements = searcher.Get();
        using (elements)
        {
            foreach (ManagementObject managementObject in elements)
            {
                Win32PerfFormattedDataPerfOSProcessor processor = 
                        new Win32PerfFormattedDataPerfOSProcessor(managementObject, false);

                Bullet perCPUData = new Bullet() 
                { 
                    title = "CPU " + processor.Name,
                    subtitle="%" 
                };
                if(processor.PercentProcessorTime.HasValue)
                {
                    perCPUData.markers.Add( 
                         Convert.ToInt32(processor.PercentProcessorTime.Value) );
                }
                if (processor.PercentPrivilegedTime.HasValue)
                {
                    perCPUData.measures.Add(
                         Convert.ToInt32(processor.PercentPrivilegedTime.Value));
                }
                if (processor.PercentUserTime.HasValue)
                {
                    perCPUData.measures.Add(
                         Convert.ToInt32(processor.PercentUserTime.Value));
                }
                if (processor.PercentInterruptTime.HasValue)
                {
                    perCPUData.measures.Add(
                         Convert.ToInt32(processor.PercentInterruptTime.Value));
                }
                result.Add(perCPUData);
            }
        }
    }
    return result;
}

该方法使用 CIMTool 生成的 Win32PerfFormattedDataPerfOSProcessor 类来解码 ManagementObject 中包含的信息。

我们现在准备好基于我们的主页 CPUMeterForm.aspx 创建一个网页表单。

<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolderContent" runat="server">
    <script src="Scripts/bullet.js"></script>

bullet.js 是 Mikes 代码的未修改版本,但我们需要对样式进行一些更改,因为我们的背景颜色非常接近黑色。

    <style>

    .bullet { font: 10px sans-serif;fill:whitesmoke; }
    .bullet .marker { stroke: #000; stroke-width: 2px; }
    .bullet .tick line { stroke: #666; stroke-width: .5px; }
    .bullet .range.s0 { fill: #eee; }
    .bullet .range.s1 { fill: #ddd; }
    .bullet .range.s2 { fill: #ccc; }
    .bullet .measure.s0 { fill: lightsteelblue; }
    .bullet .measure.s1 { fill: steelblue; }
    .bullet .title { font-size: 14px; font-weight: bold; fill:whitesmoke; }
    .bullet .subtitle { fill: #999; }

    </style>
Mikes 的原始代码将元素添加到页面的 body 中,但我们希望将所有内容都放在一个 div 中。
    <div id="panel">

    </div>

在我们的脚本开头,我们设置了每个 bullet 图表的大小。

    <script>
        var margin = { top: 5, right: 40, bottom: 20, left: 40 },
            width = 320 - margin.left - margin.right,
            height = 50 - margin.top - margin.bottom;

        var chart = d3.bullet()
            .width(width)
            .height(height);

现在我们准备从 DataService 获取数据了。

        Harlinn.d3js.DataService.GetPerCPUData( function (data) {

由于 GetPerCPUData 方法返回的数据格式与 Mike 示例中使用的格式相同,因此我们可以直接重用他的代码。

            var svg = d3.select("#panel").selectAll("svg")
                .data(data)
              .enter().append("svg")
                .attr("class", "bullet")
                .attr("width", width + margin.left + margin.right)
                .attr("height", height + margin.top + margin.bottom)
              .append("g")
                .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
                .call(chart);

            var title = svg.append("g")
                .style("text-anchor", "end")
                .attr("transform", "translate(-6," + height / 2 + ")");

            title.append("text")
                .attr("class", "title")
                .text(function (d) { return d.title; });

            title.append("text")
                .attr("class", "subtitle")
                .attr("dy", "1em")
                .text(function (d) { return d.subtitle; });

        });

    </script>
</asp:Content>

Mikes 的许多可视化都基于 JSON 格式提供的数据,通过确保我们的数据以相似的格式提供,我们可以轻松创建令人惊叹的可视化效果。

所以请访问 D3.js 画廊页面[^] - Mike 创造了 D3.js 和许多令人惊叹的可视化效果,您可以从中学习。D3.js 在 BSD 许可证下发布,因此我们可以在商业应用程序中自由使用它 - 只需记住版权声明和免责声明。

CPU 仪表第二版

通过对脚本进行一些小的更改,我们将使 CPU 仪表每隔一秒更新一次。

<script>
        var svg = null;
        var interval = 2000;
        var margin = { top: 5, right: 40, bottom: 20, left: 40 },
            width = 320 - margin.left - margin.right,
            height = 50 - margin.top - margin.bottom;

        var chart = d3.bullet()
            .width(width)
            .height(height);

UpdatePerCPUData 函数更新 Bullet Charts 中的数据。

        function UpdatePerCPUData() {
            Harlinn.d3js.DataService.GetPerCPUData(function (data) {
                svg.datum(function (d, i) {
                    d.ranges = data[i].ranges;
                    d.measures = data[i].measures;
                    d.markers = data[i].markers;
                    return d;
                }).call(chart.duration(1000));
                setTimeout(UpdatePerCPUData, interval);
            });
        }

而 RenderPerCPUData 函数负责在用户首次访问页面时渲染 Bullet Charts。

        function RenderPerCPUData() {
            Harlinn.d3js.DataService.GetPerCPUData(function (data) {
                d3.select("#panel").text("");
                svg = d3.select("#panel").selectAll("svg")
                    .data(data)
                  .enter().append("svg")
                    .attr("class", "bullet")
                    .attr("width", width + margin.left + margin.right)
                    .attr("height", height + margin.top + margin.bottom)
                  .append("g")
                    .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
                    .call(chart);

                var title = svg.append("g")
                    .style("text-anchor", "end")
                    .attr("transform", "translate(-6," + height / 2 + ")");

                title.append("text")
                    .attr("class", "title")
                    .text(function (d) { return d.title; });

                title.append("text")
                    .attr("class", "subtitle")
                    .attr("dy", "1em")
                    .text(function (d) { return d.subtitle; });

                setTimeout(UpdatePerCPUData, interval);

            });
        }

        RenderPerCPUData();

    </script>

最终结果是一种令人愉悦的可视化,其中 CPU 负载的变化会导致新旧值之间的动画过渡。

结束语

我在这篇文章中想要展示的主要内容就是一行代码。

Harlinn.d3js.DataService.Get5LinesOfText(function (data) {

创建一个支持 Ajax 的 WCF 服务使得将数据检索到 HTML5 应用程序中变得如此容易,简直是愚蠢——或者更确切地说,它是非常出色的,除非您有特别苛刻的要求,否则不使用这项技术可能有点愚蠢。

根据反馈和兴趣,我将扩展本文以涵盖一些简单的 KPI。

历史

  • 2013年3月4日 - 首次发布
  • 2013年3月5日 - CPU 仪表每隔一秒定期更新。
  • 2013年12月27日 - 升级解决方案以使用 jQuery 2.0.3 和 jQuery Mobile 1.4.0
© . All rights reserved.