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

演练:创建基于 AJAX 的单页 SharePoint 应用

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (15投票s)

2014 年 4 月 25 日

CPOL

19分钟阅读

viewsIcon

51697

downloadIcon

886

使用新的应用模型创建 SharePoint 2013 的单页应用。

目录

引言

在本文中,我将介绍 SharePoint 2013 的 SharePoint 提供程序托管应用的创建过程。您同样可以轻松实现自动托管应用,但截至 2014 年 4 月,仍无法将自动托管应用列入 Office 应用商店。

此应用与我之前的文章不同之处在于,这是一个单页、AJAX 驱动的应用程序,更像是一个真实的现代 Web 应用。

应用模型

SharePoint 应用与传统的 SharePoint 解决方案不同,它们是完全独立的。应用由两部分组成:

  • 应用安装包 - 这是用户接收并应用于其 SharePoint 安装的内容。这也是在 SharePoint 应用商店中列出的内容。它是一个简单的捆绑包,指向您的 Web 应用程序。它还可以包含任何自定义 SharePoint 组件,如列表、内容类型、事件接收器等。这些将应用于“应用 Web”——为您的应用专门创建的网站(该网站是您安装应用的网站集内的子网站)。
  • Web 应用程序 - 这是应用的“提供程序”部分。这意味着您在自己的服务器上托管它(或者如果您愿意,也可以在 Azure 上托管)。所有应用都指向它,并且实际代码在那里执行。

请注意,SharePoint 服务器上没有任何代码执行。这是完全故意的。您习惯使用的服务器对象模型现在已经消失了:不再有可能通过随意提升权限来干扰客户的 SharePoint 服务器(如果您有此习惯!)。相反,我们只能使用客户端对象模型 (CSOM)。它构建在 Web 服务之上,如果您不熟悉它,这将是一个很大的改变;我们稍后会讲到。

您仍然可以编写传统的 SharePoint 解决方案。但这可能不是一个好主意

  • 解决方案不具备未来适应性:它们已被弃用,并且可能在 SharePoint 的未来版本中不再受支持
  • SharePoint Online 只支持沙盒解决方案,而且它们受到严格限制
  • 应用可以列在 Office 应用商店中,以便轻松部署到 O365
  • 可以使用任何 Web 技术编写应用,不一定是 .NET,而是使用 OAuth 等开放 Web 标准。

点击此处了解更多应用宣传!

应用托管选项

因此,假设您已决定编写一个应用,下一步是什么?您需要做出一个重要的、初步的架构决策,即关于托管模型:有三种选项,取决于您计划提供的功能以及您应用的目標受众。

  • SharePoint 托管
    您的应用将只包含客户端代码。您可以包含自定义列表和 Web 部件,并使用 JavaScript 客户端对象模型 (CSOM) 与 SharePoint 进行交互。由于您的 JavaScript 在 SharePoint 域的上下文中执行,因此安全性非常简单。
  • 提供程序托管
    您的应用将具有托管在另一台服务器上的服务器端代码。这台服务器可以是您自己的(如果您想托管该应用),也可以是客户端本地部署的(例如,在高度安全的政府定制本地应用的情况下)。
  • 自动托管
    这是一个特殊的!架构与提供程序托管类似——应用具有服务器端代码,并且同样在 SharePoint 外部的机器上运行。但是,该机器在 Azure 上。您无需担心任何 Azure 部署细节——只需将您的应用标记为“自动托管”,当您安装它时,Azure 实例会自动创建并连接——就像变魔术一样。请注意,截至 2014 年 4 月,自动托管应用仍未被 Office 应用商店接受。您仍然可以向用户发送应用包进行手动安装,但基础架构似乎尚未 100% 定型,这让我有些担心。

查看此 MSDN 文章,了解更深入的托管选项。

本文将重点介绍 SharePoint 2013 Online 的**提供程序托管**应用的创建。

必备组件

您将需要:

  • Visual Studio 2013。您也可以使用 Visual Studio 2012 + Office Developer Tools 开发应用,但 MVC 项目模板默认不可用。
  • SharePoint Online (O365) 并已配置开发站点(忽略有关 Napa 的内容,我们有 Visual Studio!)。

创建解决方案

我们将创建一个简单的“待办事项”应用,灵感来自TodoMVC项目。

让我们开始吧!点击 **文件 -> 新建 -> 项目**,然后选择 **SharePoint 2013 应用**

输入您的开发站点的 URL,然后选择 **提供程序托管**

在下一个屏幕上,选择 **ASP.NET MVC Web 应用程序**

下一个屏幕提供身份验证选项

您的身份验证机制选择将取决于您应用的目标受众

  • Windows Azure 访问控制服务:
    这适用于将部署到 SharePoint Online (Office 365) 或通过 Office 应用商店分发应用。
  • 证书(高信任):
    用于无法访问 Office 365 租户的本地安装。这些高信任应用无法在 Office 应用商店中列出。点击此处了解更多信息。

保留 Windows Azure 访问控制服务(默认选项),然后点击完成。

项目结构

此时将为您创建两个项目。

  • TodoApp - 此项目包含托管在 SharePoint 上的应用部分。它包含自定义列表或 Web 部件的任何定义,并定义您的应用所需的权限。
  • TodoAppWeb - 这是 Web 项目,代表将要执行的内容。此项目包含您的代码!

查看 TodoAppWeb 项目。为我们生成了很多代码!我们主要关心的区域是:

  • Controllers:MVC 的 C:处理模型和视图(在本例中为数据库和网页)之间的交互
  • Filters:一种在渲染特定页面时执行代码的便捷方式。我们将使用一个来确保用户在每个请求开始时都已登录 SharePoint
  • Models:MVC 的 M!在本例中,一个类代表每个数据库表
  • Scripts:我们所有的 JavaScript 都位于此文件夹中。
  • Views:包含每个页面的 .cshtml 文件——包含 HTML 和 Razor 视图呈现代码。

无需执行任何操作,即可按 F5 部署您的应用。您可能需要登录您的开发站点。部署后,将打开一个浏览器窗口,SharePoint 将要求您批准您的应用。

默认只请求基本权限。点击 **信任它**,您将被重定向到此处。

现在您已成功运行了一个应用。

单页应用

与我之前的文章不同,该文章遵循标准的 Microsoft MVC 模式和实践,此应用将是单页应用,并且主要基于 JavaScript,利用 ajax 和 Web 服务。如前所述,我们将基于TodoMVC 项目。更具体地说,我们将使用 TodoMVC 应用的 Knockout 版本。因此,在此处从 GitHub 下载 knockout todomvc 应用,并按如下方式将其集成到您的项目中:

  • 将 **js** 和 **bower_components** 文件夹复制到 **Scripts** 文件夹。要快速完成此操作:
    1. 在 Windows Explorer 中复制文件夹
    2. Visual Studio,启用 **项目 -> 显示所有文件**
    3. 文件夹现在将显示在解决方案资源管理器中。右键单击 **bower_components**,然后选择 **包含在项目中**。对 **js** 文件夹也执行相同操作。
  • 将 **index.html** 的内容复制到 **Views/Index.cshtml**(丢弃其中的任何内容)。
  • 打开 **Views/Index.cshtml** 并编辑脚本和 CSS 引用,使其指向 Scripts 文件夹(例如,查找指向 **bower_components** 的任何引用,并将其更改为 **Scripts/bower_components**,对 **js** 也执行相同操作)。
  • 打开 Shared/_Layout.cshtml 文件,并用对 **@RenderBody():** 的单个调用替换其内容:

您的解决方案现在应如下所示:

按 **F5**,您现在应该有一个正在运行的 TodoMVC 应用!

数据存储

在之前的文章中,我描述了如何使用 Azure 数据库来存储您的数据。在提供程序托管的应用中,您可以同样轻松地将数据存储在自己的数据库中。但是,除了性能问题之外,如果可能,将数据存储在客户自己的 SharePoint 系统中确实非常方便。这有几个优点:

  • 安全性 - 您无需将客户数据存储在自己的数据中心
  • 应用互操作性 - 如果有多个应用与 SharePoint 通信,使用 SharePoint 作为中央数据存储将大大简化架构。
  • 透明度 - 客户可以在列表中透明地查看其数据,并更好地了解存储了哪些数据以及如何使用。
  • 与 SharePoint 紧密集成 - 这通常很有用,因为之后您可以轻松利用工作流和事件接收器等功能。

因此,在本文中,我将使用 SharePoint 列表进行数据存储。让我们创建一个列表来存储我们的待办事项。首先,右键单击 TodoApp 项目,然后选择 **添加 -> 新建项**。选择 **列表**,并将其命名为 **TodoList**。

点击 **添加**。在下一个对话框中,将列表模板保留为 **默认**,然后点击 **完成**。

现在将显示一个列表设计器表单。默认情况下,它已经有一个 Title 列,因此只需添加另一个名为 **Completed** 的布尔列即可。

在我的例子中,Completed 可作为站点列使用。但是,它默认是隐藏的。要解决此问题,请打开 **Schema.xml**,并确保 **Completed** 字段具有 **Hidden=FALSE**

再次打开 TodoListInstance 设计器,点击“视图”选项卡,并将 Completed 列添加到默认视图中。

现在我们将添加用于检索列表项的服务器端代码。首先,添加一个名为 TodoItemViewModel 的类,并为其提供相关的属性<!--。请注意,属性未大写,以匹配我们客户端已有的内容-->

    public class TodoItemViewModel
    {
        public string Title { get; set; }
        public bool Completed { get; set; }
    }
    

接下来,打开 HomeController 类,并修改 Index 方法以加载 TodoList 的内容。

    [SharePointContextFilter]
    public ActionResult Index()
    {
        List<TodoItemViewModel> result = new List<TodoItemViewModel>();
        var spContext = SharePointContextProvider.Current.GetSharePointContext(HttpContext);
        using (var clientContext = spContext.CreateAppOnlyClientContextForSPAppWeb())
        {
            if(clientContext != null)
            {
                //Load list items
                List list = clientContext.Web.Lists.GetByTitle("TodoList");
                ListItemCollection items = list.GetItems(CamlQuery.CreateAllItemsQuery());
                clientContext.Load(items);
                clientContext.ExecuteQuery();
                //Create the Todo item view models
                result = items.Select(li => new TodoItemViewModel()
                {
                    Title = (string)li["Title"],
                    Completed = (bool)li["Completed"]
                }).ToList();
            }
        }
        return View(result); //Pass the items into the view
    }
    

您可能会注意到我们使用的是 **CreateAppOnlyClientContextForSPAppWeb**。这意味着我们正在以应用身份访问列表,而不是用户身份。这是为了避免要求用户本身拥有任何特殊权限——这应该是您应用中一直以来的重要考虑因素,因为不同的资源可能仅对某些用户可用,这构成了您安全模型的重要组成部分。

现在,由于我们要以应用身份访问列表,因此需要通过打开 **TodoApp** 下的 **AppManifest.xml** 来启用它,点击 **权限** 选项卡,然后勾选该选项。

使用 Knockout.js 的客户端代码

现在我们转向客户端。我们将做一些有点可怕的事情,重写 **app.js** 文件,其中包含我们导入的所有待办事项应用 JavaScript。我们将重写它,以便首先理解它,其次,我们能够更轻松地将其与我们的 AJAX 方法集成。您始终可以稍后返回研究原始代码,因为它会更高级、更完善。

如果您是 JavaScript 中 Knockout 数据绑定方式的新手,现在可能是访问 knockout.js 网站以熟悉它的好时机。它非常强大,而且相当容易上手,并且使编写 JavaScript 应用程序变得异常快速和简单。

因此,打开 **app.js**,删除其中的内容,然后让我们开始创建一个简单的待办事项视图模型。

    window.TodoApp = window.TodoApp || {};
    
    window.TodoApp.Todo = function (id, title, completed) {
        var me = this;
        this.id = ko.observable(id);
        this.title = ko.observable(title);
        this.completed = ko.observable(completed || false);
        this.editing = ko.observable(false);

        // edit an item
        this.startEdit = function () {
            me.editing(true);
        };

        // stop editing an item
        this.stopEdit = function (data, event) {
            if (event.keyCode == 13) {
                me.editing(false);
            }
            return true;
        };
    };

    

接下来,我们添加主视图模型。

    window.TodoApp.ViewModel = function (spHostUrl) {
        var me = this;
        this.todos = ko.observableArray(); //List of todos
        this.current = ko.observable(); // store the new todo value being entered
        this.showMode = ko.observable('all'); //Current display mode

        //List which is currently displayed
        this.filteredTodos = ko.computed(function () {
            switch (me.showMode()) {
            case 'active':
                return me.todos().filter(function (todo) {
                    return !todo.completed();
                });
            case 'completed':
                return me.todos().filter(function (todo) {
                    return todo.completed();
                });
            default:
                return me.todos();
            }
        });        

        this.addTodo = function (todo) {
            me.todos.push(todo);
        }

        // add a new todo, when enter key is pressed
        this.add = function (data, event) {
            if (event.keyCode == 13) {
                var current = me.current().trim();
                if (current) {        
                    var todo = new window.TodoApp.Todo(0, current);
                    me.addTodo(todo);
                    me.current('');
                }
            }
            return true;
        };

        // remove a single todo
        this.remove = function (todo) {
            me.todos.remove(todo);
        };

        // remove all completed todos
        this.removeCompleted = function () {
            var todos = me.todos().slice(0);
            for (var i = 0; i < todos.length; i++) {
                if (todos[i].completed()) {
                    me.remove(todos[i]);
                }
            }
        };

        // count of all completed todos
        this.completedCount = ko.computed(function () {
            return me.todos().filter(function (todo) {
                return todo.completed();
            }).length;
        });

        // count of todos that are not complete
        this.remainingCount = ko.computed(function () {
            return me.todos().length - me.completedCount();
        });

        // writeable computed observable to handle marking all complete/incomplete
        this.allCompleted = ko.computed({
            //always return true/false based on the done flag of all todos
            read: function () {
                return !me.remainingCount();
            },
            // set all todos to the written value (true/false)
            write: function (newValue) {
                me.todos().forEach(function (todo) {
                    // set even if value is the same, as subscribers are not notified in that case
                    todo.completed(newValue);
                });
            }
        });
    };

请仔细阅读上面的 JavaScript - 我试图将其从原始的 TodoMVC Knockout 代码中简化,所以如果您是 Knockout 的新手,它应该更容易理解。

接下来,我们将更改 HTML 以匹配我们更新的 JavaScript。打开 **index.cshtml** 并用以下内容替换 **<body>** 标签的全部内容。

    <section id="todoapp">
        <header id="header">
            <h1>todos</h1>
            <input id="new-todo" data-bind="value: current, valueUpdate: 'afterkeydown', event: { keypress: add }" placeholder="What needs to be done?" autofocus>
        </header>
        <section id="main" data-bind="visible: todos().length">
            <input id="toggle-all" data-bind="checked: allCompleted" type="checkbox">
            <label for="toggle-all">Mark all as complete</label>
            <ul id="todo-list" data-bind="foreach: filteredTodos">
                <li data-bind="css: { completed: completed, editing: editing }">
                    <div class="view">
                        <input class="toggle" data-bind="checked: completed" type="checkbox">
                        <label data-bind="text: title, event: { dblclick: startEdit }"></label>
                        <button class="destroy" data-bind="click: $root.remove"></button>
                    </div>
                    <input class="edit" data-bind="value: title, valueUpdate: 'afterkeydown', event: { keypress: stopEdit }">
                </li>
            </ul>
        </section>
        <footer id="footer" data-bind="visible: completedCount() || remainingCount()">
            <span id="todo-count">
                <strong data-bind="text: remainingCount">0</strong> item(s) left
            </span>
            <ul id="filters">
                <li>
                    <a data-bind="css: { selected: showMode() == 'all' }, click: function(){showMode('all');}">All</a>
                </li>
                <li>
                    <a data-bind="css: { selected: showMode() == 'active' }, click: function(){showMode('active');}">Active</a>
                </li>
                <li>
                    <a data-bind="css: { selected: showMode() == 'completed' }, click: function(){showMode('completed');}">Completed</a>
                </li>
            </ul>
            <button id="clear-completed" data-bind="visible: completedCount, click: removeCompleted">
                Clear completed (<span data-bind="text: completedCount"></span>)
            </button>
        </footer>
    </section>
    <footer id="info">
        <p>Double-click to edit a todo</p>
        <p>Written by <a href="https://github.com/ashish01/knockoutjs-todos">Ashish Sharma</a> and <a href="http://knockmeout.net">Ryan Niemeyer</a></p>
        <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
    </footer>
    <script src="Scripts/bower_components/todomvc-common/base.js"></script>
    <script src="Scripts/bower_components/knockout.js/knockout.debug.js"></script>
    <script src="Scripts/jquery-1.10.2.js"></script>
    <script src="Scripts/js/app.js"></script>
        
    <script>
        var viewModel = new window.TodoApp.ViewModel(spHostUrl);
        ko.applyBindings(viewModel);
    </script>

同样,这比原始的 TodoMVC 代码稍作简化。

您现在应该能够像以前一样运行应用了。在下一步中,我们将添加加载服务器数据所需的客户端代码。更新 **index.cshtml** 中的最后一个脚本块,使其与以下内容匹配:

        
    <script>
        var model = @(Html.Raw(Json.Encode(Model)));
        var viewModel = new window.TodoApp.ViewModel();

        for(var i=0; i<model.length; i++) {
            viewModel.addTodo(new window.TodoApp.Todo(model[i].Id, model[i].Title, model[i].Completed));
        }

        ko.applyBindings(viewModel);
    </script>

    

现在我们准备对应用进行快速测试。按 **F5** 并等待应用打开。由于我们还没有保存数据的方法,我们将直接在 SharePoint 列表中添加一些数据。只需使用 URL **/[YourSPsite]/TodoApp/Lists/TodoList/** 浏览到列表,您应该会看到标准的列表 UI。

添加几个测试项并刷新您的应用。

通过 AJAX 保存数据

最后一块是响应用户输入并将数据保存回服务器。当一个项被删除时,我们需要知道要删除的 SharePointListItem 的 Id。因此,我们将向 **TodoItemViewModel** 类添加一个 **Id** 属性。

    public class TodoItemViewModel
    {
        public int Id { get; set; } //New!
        public string Title { get; set; }
        public bool Completed { get; set; }
    }
    

别忘了在 HomeController Index 方法加载项时设置 Id 属性。

现在我们将创建用于添加、删除和更新项的 Web 服务方法。将这些方法添加到 HomeController。

    [HttpPost]
    public JsonResult AddItem(string title)
    {
        int newItemId = 0;
        var spContext = SharePointContextProvider.Current.GetSharePointContext(HttpContext);
        using (var clientContext = spContext.CreateAppOnlyClientContextForSPAppWeb())
        {
            if (clientContext != null)
            {
                List list = clientContext.Web.Lists.GetByTitle("TodoList");
                ListItem listItem = list.AddItem(new ListItemCreationInformation());
                listItem["Title"] = title;
                listItem.Update();
                clientContext.Load(listItem, li => li.Id);
                clientContext.ExecuteQuery();
                newItemId = listItem.Id;
            }
        }
        return Json(newItemId, JsonRequestBehavior.AllowGet);
    }

    [HttpPost]
    public JsonResult RemoveItem(int id)
    {
        var spContext = SharePointContextProvider.Current.GetSharePointContext(HttpContext);
        using (var clientContext = spContext.CreateAppOnlyClientContextForSPAppWeb())
        {
            if (clientContext != null)
            {
                List list = clientContext.Web.Lists.GetByTitle("TodoList");
                ListItem item = list.GetItemById(id);
                item.DeleteObject();
                clientContext.ExecuteQuery();
            }
        }
        return new JsonResult();
    }

    [HttpPost]
    public JsonResult UpdateItem(int id, string title, bool completed)
    {
        var spContext = SharePointContextProvider.Current.GetSharePointContext(HttpContext);
        using (var clientContext = spContext.CreateAppOnlyClientContextForSPAppWeb())
        {
            if (clientContext != null)
            {
                List list = clientContext.Web.Lists.GetByTitle("TodoList");
                ListItem item = list.GetItemById(id);
                item["Title"] = title;
                item["Completed"] = completed;
                item.Update();
                clientContext.ExecuteQuery();
            }
        }
        return new JsonResult();
    }
    

以上方法使用相对简单的 CSOM 代码来添加、删除和更新列表项。您可以通过使用 JavaScript 版本的 CSOM 来实现相同的目标,这将带来调用 SharePoint 而不通过应用服务器的额外好处。但是,我没有这样做,因为我想说明如何进行应用客户端和服务器之间的 AJAX 调用。点击此处阅读有关 JavaScript CSOM 库的更多信息

现在我们将添加 JavaScript 代码,以便在发生添加、更新或删除操作时调用这些服务器方法。您可能已经注意到我们包含了 jQuery 脚本的引用。这是因为我们将使用 jQuery 的 AJAX 辅助方法。

现在,我们只需要添加代码来响应添加、删除和更新事件,并调用服务器方法。第一步是打开 **HomeController.cs** 并将 **SPHostUrl**(这是身份验证的关键值)添加到 ViewBag 中。

这样做的目的是让我们能够在客户端访问 SPHostUrl,并在 AJAX 请求期间将其传回服务器。身份验证 cookie 也将被传递,两者结合起来构成了身份验证所需的一切。

接下来,打开 **Index.cshtml** 文件,并将最后一个脚本块更新为与以下内容匹配:

    <script>
        var model = @(Html.Raw(Json.Encode(Model)));
        
        var spHostUrl = '@ViewBag.SPHostUrl'; //<-- Add this line
        var viewModel = new window.TodoApp.ViewModel(spHostUrl); <-- Pass SPHostUrl into the ViewModel

        for(var i=0; i<model.length; i++) {        
            viewModel.addTodo(new window.TodoApp.Todo(model[i].Id, model[i].Title, model[i].Completed));
        }
        ko.applyBindings(viewModel);
    </script>
    

接下来,我们将更新 **app.js** 代码,通过使用 jQuery 的 **$.ajax** 辅助方法添加 HTTP 请求,在适当的时候调用 Web 服务。首先,更新 **add** 函数。

    this.add = function (data, event) {
        if (event.keyCode == 13) {
            var current = me.current().trim();
            if (current) {
                var todo = new window.TodoApp.Todo(0, current);
                me.addTodo(todo);
                me.current('');

                $.ajax({
                    type: 'POST',
                    url: "/Home/AddItem?SPHostUrl=" + encodeURIComponent(spHostUrl),
                    contentType: "application/json; charset=utf-8",
                    data: JSON.stringify({
                        title: todo.title()
                    }),
                    dataType: "json",
                    success: function (id) {
                        todo.id(id);
                    }
                });
            }
        }
        return true;
    };
    

接下来是 **remove** 函数。

    this.remove = function (todo) {
        me.todos.remove(todo);
        $.ajax({
            type: 'POST',
            url: "/Home/RemoveItem?SPHostUrl=" + encodeURIComponent(spHostUrl),
            contentType: "application/json; charset=utf-8",
            data: JSON.stringify({
                id: todo.id()
            }),
            dataType: "json"
        });
    };
    

现在添加和删除已处理,只剩下处理更新。我们将通过在 **addTodo** 函数中添加代码来做到这一点,该函数会侦听 **title** 或 **completed** 属性的变化,并将更改提交到服务器。更新后的 **addTodo** 函数应如下所示:

    this.addTodo = function (todo) {
        me.todos.push(todo);
        var adding = true;
        ko.computed(function () {
            var title = todo.title(),
                completed = todo.completed();
            if (!adding) {
                $.ajax({
                    type: 'POST',
                    url: "/Home/UpdateItem?SPHostUrl=" + encodeURIComponent(spHostUrl),
                    contentType: "application/json; charset=utf-8",
                    data: JSON.stringify({
                        id: todo.id(),
                        title: title,
                        completed: completed
                    }),
                    dataType: "json"
                });
            }
        });
        adding = false;
    }
    

在上面的代码中,我们添加了一个 Knockout 计算属性,它会侦听 **title** 和 **completed** 可观察属性。如果任何值发生更改,我们将调用 **UpdateItem** Web 服务。请注意,我们使用名为 **adding** 的标志来确保在初始项添加期间不调用 **UpdateItems**。

再次运行项目。如果一切顺利,您应该能够插入、编辑和删除项,并将它们立即保存回 SharePoint。

整理

现在是时候进行一些整理,以确保我们遵循 MVC 约定。首先,我们应该使用脚本加载器来确保 JavaScript 的加载效率最高。打开 **App_Start\BundleConfig.cs** 并用以下内容替换其内容:

    public static void RegisterBundles(BundleCollection bundles)
    {
        bundles.Add(new ScriptBundle("~/bundles/scripts").Include(
                    "~/Scripts/bower_components/todomvc-common/base.js",
                    "~/Scripts/bower_components/knockout.js/knockout.debug.js",
                    "~/Scripts/jquery-{version}.js",
                    "~/Scripts/js/app.js"));

        bundles.Add(new StyleBundle("~/Content/css").Include(
                    "~/Scripts/bower_components/todomvc-common/base.css"));
    }

这会创建两个捆绑包:一个 CSS 捆绑包和一个 JavaScript 捆绑包。优点是,在生产环境中,您的脚本将在单个请求中加载,并且可以轻松地进行最小化处理。通过在 **<head>** 标签中引用您的捆绑包来更新您的 **Index.cshtml** 文件,删除现有的脚本和样式引用。

    <head>
        <meta charset="utf-8">
        <title>Knockout.js TodoMVC</title>
        @Styles.Render("~/Content/css")
        @Scripts.Render("~/bundles/scripts")
    </head>
    

由于您的应用不使用默认项目模板中包含的 **About** 或 **Contact** 页面,因此您可以删除它们。

同样,删除 **HomeController** 类中相关的​​方法。

添加应用部件 (WebPart)

添加 Web 部件非常容易!您可以创建一个应用部件来指向您应用中的任何页面,它只需在 SharePoint 站点内的 iframe 中显示。实际上,您可能希望创建一个新页面。我们将创建一个样式与应用略有不同的页面。

现在,此视图将与主 **Index** 视图相同,只是样式不同。我们不想将原始视图复制粘贴到此视图中,因此我们将创建一个 **Shared** 视图供两者使用。

首先,通过右键单击 **Views/Home** 文件夹并选择 **添加 -> 视图:** 来添加一个新视图,并将其命名为 IndexCommon。

现在**复制 Index 的整个 body 标签内容**到 **IndexCommon**。然后,您可以使用对 **@Html.Partial** 的调用从 Index 中引用 IndexCommon。您的 **Index.cshtml** 应如下所示:

    <!doctype html>
    <html lang="en" data-framework="knockoutjs">
    <head>
        <meta charset="utf-8">
        <title>Knockout.js TodoMVC</title>
        @Styles.Render("~/Content/css")
        @Scripts.Render("~/bundles/scripts")
    </head>
    <body>
        @Html.Partial("IndexCommon")
    </body>
    </html>
    

添加一个名为 **IndexTrimmed** 的新视图。

同样,对 Index 使用相同的内容。但这次我们将做一个改变,就是引用一个备用的 CSS 文件。所以将 **@Styles.Render** 标签更改为如下所示:

现在打开 **App_Start/BundleConfig.cs** 并添加一个新捆绑包。

    
    bundles.Add(new StyleBundle("~/Content/css-trimmed").Include(
                "~/Scripts/bower_components/todomvc-common/base-trimmed.css"));
    

并通过复制 **base.css** 到 **base-trimmed.css** 来添加 CSS 文件。

此时您可以修改此 CSS 文件。我删除了以下规则:

  • #todoapp { margin: 130px 0 40px 0; } (顶部的巨大标题)
  • body { margin: 0 auto; } (这导致页面居中)
  • background: #eaeaea url('bg.png'); (灰色背景图片)

我添加了以下规则:

  • #info { display:none; } (隐藏信息页脚)

下一步是为我们的新视图添加一个 Controller 方法。打开 HomeController 并添加一个名为 **IndexTrimmed** 的方法。它应该与 **Index** 包含完全相同的内容,所以我将其提取到一个通用方法中。

    [SharePointContextFilter]
    public ActionResult IndexTrimmed()
    {
        return View(GetItems()); //Pass the items into the view
    }

    [SharePointContextFilter]
    public ActionResult Index()
    {
        return View(GetItems()); //Pass the items into the view
    }

    private List<TodoItemViewModel> GetItems()
    {
        List<TodoItemViewModel> result = new List<TodoItemViewModel>();
        var spContext = SharePointContextProvider.Current.GetSharePointContext(HttpContext);
        ViewBag.SPHostUrl = spContext.SPHostUrl;
        using (var clientContext = spContext.CreateAppOnlyClientContextForSPAppWeb())
        {
            if (clientContext != null)
            {
                //Load list items
                List list = clientContext.Web.Lists.GetByTitle("TodoList");
                ListItemCollection items = list.GetItems(CamlQuery.CreateAllItemsQuery());
                clientContext.Load(items);
                clientContext.ExecuteQuery();
                //Create the Todo item view models
                result = items.ToArray().Select(li => new TodoItemViewModel()
                {
                    Id = (int)li.Id,
                    Title = (string)li["Title"],
                    Completed = (bool)li["Completed"]
                }).ToList();
            }
        }
        return result;
    }

现在我们准备添加 Web 部件。右键单击解决方案资源管理器中的 TodoApp,然后选择 **添加** -> **新建项**。选择 **客户端 Web 部件**。

为其命名,然后点击 **下一步:**。我们想使用我们的新页面,所以请输入其 URL。

打开 **TodoApp/TodoWebPart/Elements.xml** 文件,并将默认宽度从 300px 更改为 600px。

现在再次运行您的应用。确保整个应用重新安装,因为应用部件安装在 SharePoint 服务器本身上。通过访问您的 SharePoint 站点并点击 **页面**,然后点击 **编辑** 来添加应用部件。

然后选择 **插入 -> 应用部件,并从列表中选择 TodoWebPart。点击添加。**

点击 **保存** 以保存您对页面的更改,此时应该会显示您的 Web 部件。

使您的 Web 部件动态调整大小

现在,您会注意到当您向列表中添加项时,Web 部件没有正确调整大小。这是因为随着应用本身变大,应用部件不会变大——毕竟它是一个固定高度的 iframe。对于不调整大小的应用来说,这不成问题,例如表单或固定长度的列表。但是,我们希望我们的应用像主网站流中的普通部分一样调整大小。

幸运的是,这有一个变通方法,涉及将消息发布到 iframe 并要求它调整大小。

将以下代码添加到您的 **HomeController** 中的 **IndexTrimmed** 方法。

    public ActionResult IndexTrimmed()
    {
        ViewBag.SenderId = HttpContext.Request.Params["SenderId"];
        return View(GetItems());
    }
    

上述代码的目的是从 URL 中检索 SenderId 参数,并通过 **ViewBag** 将其提供给客户端。这个 SenderId 参数代表托管应用部件的 iframe 的 ID。

接下来,我们将使用客户端中的 Sender ID 来请求 iframe 在应用调整大小时调整大小——更具体地说,每当向列表添加或从中删除项时。此脚本应添加到 **IndexTrimmed.cshtml** 的 body 末尾。

    //Retrieve the sender ID from the viewbag
    var senderId = '@ViewBag.SenderId';

    //Create a function that will change the height of the iframe
    function setHeight() {
        var width = $('#todoapp').outerWidth(true),
            height = $('#todoapp').outerHeight(true);

        //Notify SP that it should resize its iframe to the appropriate height.
        window.parent.postMessage('<message senderId=' + senderId + '>resize(' + width + ', ' + (height + 50) + ')</message>', "*");
    }

    //List for changes to the todo list, and call setHeight when that happens
    viewModel.todos.subscribe(setHeight);

    //Set the correct initial height when the app first loads.
    setHeight();
    

为避免混淆,脚本应放在此处:

该脚本故意放在调用加载 IndexCommon 局部视图的调用之后,因为它将能够访问 JavaScript **viewModel** 对象。

打包您的应用

对于自动托管应用,过程更简单:只需右键单击您的应用并选择发布。然后,点击打包,您将获得一个 **.app** 文件。此应用文件包含所有内容:SharePoint 应用和远程应用(您的 Web 项目)。它非常简单,因为自动托管过程会负责创建 Client ID 和 Client Secret(用于 OAuth 身份验证),并负责物理托管。

对于提供程序托管的应用,情况更为复杂,您遵循的步骤取决于您想如何托管您的 Web 项目。您的第一步将是访问此页面,通过注册您的应用来创建 Client ID 和 Client Secret。完成后,您可以右键单击您的应用项目并点击打包。然后按照发布向导进行操作,该向导将涉及输入您的 Client ID 和 Client Secret。

请注意,对于您的提供程序托管应用,您将单独部署您的 Web 项目和您的应用项目。这显而易见:您将自己托管您的 Web 项目(毕竟您是*提供程序*),而小型应用项目将被打包并在 Office 应用商店中列出,或手动发送给客户。一旦他们安装了它,它只会指向您安装了 Web 应用的 Web 服务器。要正确配置此项,请在应用项目中打开 **AppManifest.xml**,并输入您的应用已安装的 URL。

下载

点击此处下载完整的应用解决方案 Zip 文件。

结束

我们已经了解了如何创建 SharePoint 2013 的提供程序托管应用,并且创建自动托管应用的过程大致相同。过程中存在许多复杂之处,本文仅触及皮毛。例如,我没有涵盖任何有关特殊应用权限的内容。但本文涵盖了大部分重要内容:MVC、Knockout 和 Web 服务。这应该能让您具备创建出色应用的工具。有关更具商业导向的 SharePoint 应用类型,请参阅我的另一篇文章。祝您好运!

© . All rights reserved.