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

SharePoint 2013 客户端渲染:列表表单

starIconstarIconstarIconstarIconstarIcon

5.00/5 (10投票s)

2014年8月29日

CPOL

8分钟阅读

viewsIcon

127696

与列表表单相关的客户端呈现具体细节。

引言

客户端呈现 (CSR) 在 SharePoint 2013 中被引入,作为一种主要的显示数据技术,取代了 XSLT。CSR 现在默认用于显示标准列表(例外:调查列表和日历列表)中的数据 - 包括列表表单、列表视图和快速编辑模式下的列表 - 以及搜索结果。

尽管 CSR 被重用于显示所有这些,但不同数据源的处理过程却有显著差异。我在上一篇文章中解释了 CSR 的基础知识以及 CSR 在列表视图中的具体应用:SharePoint 2013 客户端呈现:列表视图。在本文中,我将重点介绍列表表单:它们的工作原理、常见问题以及如何使用它们。本文包含 4 个代码示例。

  • 基本示例:自定义字段值的显示方式
  • 基本示例:自定义字段控件
  • 示例:依赖字段
  • 示例:操作表单布局

列表表单的 CSR

SharePoint 2013 中的列表表单可以使用 3 种模式进行呈现:标准、自定义和服务器呈现。

.

服务器呈现回退到 XSLT,而标准和自定义模式都基于 CSR。

标准模式

在标准模式下,ListFormWebPart 会渲染表单的模板,包括表格结构甚至字段标题。作为字段值或控件的占位符,ListFormWebPart 会渲染空的 span 元素,并带有唯一的标识符,如下所示。

在页面加载期间,一个简化的 CSR 过程会对表单中的每个字段反复执行,并且在每个字段呈现后,相应的 span 元素将被 CSR 进程生成的 HTML 替换。

表单的呈现由 RenderForm 方法处理。给定字段的简化 CSR 过程和 span 内容的替换由 RenderReplace 方法执行。

我称标准模式下的 CSR 为“简化”,因为它并非处理所有阶段。视图、页眉、页脚、正文和组处理程序永远不会执行。因此,无法通过 CSR 模板更改字段的布局。在此模式下更改表单布局的唯一方法是重新排列已呈现的 DOM 元素,例如使用 jQuery。

显然,标准模式主要用于自定义字段控件/值的呈现。

基本示例:自定义字段值的显示方式

以下代码自定义了在显示表单中字段值的显示方式。

SPClientTemplates.TemplateManager.RegisterTemplateOverrides({

  Templates: {

    Fields: {
      'Title': {
        DisplayForm: function(ctx) {
          return '<div class="my-field-title">' + ctx.CurrentItem.Title + '</div>';
        }
      }
    }

  }

});

结果

FormContext

与列表视图 CSR 相比,列表表单的 CSR 过程稍微复杂一些,因为列表表单除了显示之外,还必须提供编辑功能。很明显,为了执行值的编辑,您必须与 CSR 核心进行交互,至少要向其提供输入到自定义控件中的值。

这是通过 ctx.FormContext.updateControlValue(fieldName, value) 函数实现的。ctx.FormContext 对象还包含许多其他有用的信息,用于呈现表单和处理其值。

ctx.FormContext 的另一个有用函数是 registerInitCallback(fieldName, callback)。它比 PostRender 更方便使用,尽管本质上做的事情相同:在字段创建后立即执行提供的回调。

ctx.FormContextClientFormContext 类的实例。以下是该类的完整定义。

    class ClientFormContext {
        fieldValue: any;
        fieldSchema: SPClientTemplates.FieldSchema_InForm;
        fieldName: string;
        controlMode: number;
        webAttributes: {
            AllowScriptableWebParts: boolean;
            CurrentUserId: number;
            EffectivePresenceEnabled: boolean;
            LCID: string;
            PermissionCustomizePages: boolean;
            WebUrl: string;
        };
        itemAttributes: {
            ExternalListItem: boolean;
            FsObjType: number;
            Id: number;
            Url: string;
        };
        listAttributes: {
            BaseType: number;
            DefaultItemOpen: number;
            Direction: string;
            EnableVesioning: boolean;
            Id: string;
        };
        registerInitCallback(fieldname: string, callback: () => void ): void;
        registerFocusCallback(fieldname: string, callback: () => void ): void;
        registerValidationErrorCallback(fieldname: string, callback: (error: any) => void ): void;
        registerGetValueCallback(fieldname: string, callback: () => any): void;
        updateControlValue(fieldname: string, value: any): void;
        registerClientValidator(fieldname: string, 
		validator: SPClientForms.ClientValidation.ValidatorSet): void;
        registerHasValueChangedCallback(fieldname: string, callback: (eventArg?: any) => void );
    }

此定义取自开源项目 TypeScript Definitions for SharePoint 2013。使用此项目和 TypeScript,您可以为 CSR 代码获得智能感知和类型检查。TypeScript 然后会编译成 JS。

基本示例:自定义字段控件

这是代码

  function MyFieldControl(ctx) {

    // Instead of hardcoding the field value, we can fetch it from the context
    var fieldInternalName = ctx.CurrentFieldSchema.Name;
    var controlId = fieldInternalName + "_control";

    // Initialization of the field: here I'm attaching onchange event to my control,
    // so that whenever text is changed, FormContext.updateControlValue is executed.
    ctx.FormContext.registerInitCallback(fieldInternalName, function () {

        $addHandler($get(controlId), "change", function(e) {
            ctx.FormContext.updateControlValue(fieldInternalName, $get(controlId).value);
        });

    });

    return String.format('<input type="text" 
    id="{0}" name="{0}" class="my-input" />', controlId);
  };

  SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
    Templates: {
       Fields: {
         'Title': {
           EditForm: MyFieldControl,
           NewForm: MyFieldControl
         }
       }
    }

  });

$get$addHandler 函数来自 ASP.NET Ajax 库,String.format 也是如此。此库默认部署到每个 SharePoint 页面。如果您愿意,可以删除这些函数,并用 jQuery、纯 JS 或任何您喜欢的其他内容替换它们。

现在这段代码将可以工作,并且已经尽可能简化。然而,建议在 MyFieldControl 模板处理程序中添加另外两个调用:registerFocusCallbackregisterValidationErrorCallback。这将确保自定义字段的一致行为。所以这是处理程序的最终版本。

function MyFieldControl(ctx) {

    var fieldInternalName = ctx.CurrentFieldSchema.Name;
    var controlId = fieldInternalName + "_control";

    ctx.FormContext.registerInitCallback(fieldInternalName, function () {
        $addHandler($get(controlId), "change", function(e) {
            ctx.FormContext.updateControlValue(fieldInternalName, $get(controlId).value);
        });
    });

    ctx.FormContext.registerFocusCallback(fieldInternalName, function() {
        $get(controlId).focus();
    });

    ctx.FormContext.registerValidationErrorCallback(fieldInternalName, function(errorResult) {
        SPFormControl_AppendValidationErrorMessage(controlId, errorResult);
    });

    return String.format('<input type="text" id="{0}" 
    name="{0}" class="my-input" />', controlId);
};

到目前为止还不错。但有没有更有趣、更贴近实际的呢?如果我们还需要字段之间的交互,以便更改一个字段,另一个字段相应地随之改变呢?

示例:依赖字段

依赖字段通常是 SharePoint 列表表单最受欢迎的功能之一。现在有了 CSR,我们终于有了一种非常好的方法,仅使用开箱即用的功能来实现它们。

考虑您有汽车,并且每辆汽车可以有不同的颜色。每次选择一辆汽车时,您都应该检查当前该汽车有哪些可用颜色,并隐藏不相关的颜色。

类似这样的东西

也许最简单的方法是使用 OnPostRender 事件。在这种情况下,您无需重新定义任何内容 - 因此代码量会更少;但另一方面,您必须了解开箱即用的字段是如何呈现的,以便在呈现后对其进行操作。

于是我打开了 Internet Explorer 中的开发者工具窗口,并快速找到了我可以跟踪的 select 控件的 ID。

显然,这里的字段 ID、字段类型和字段名称被组合成了控件 ID。所有这些信息都可以从上下文对象中获取,因此在代码方面,它看起来像这样。

var f = ctx.ListSchema.Field[0];
var fieldControlId = f.Name + "_" + f.Id + 
"_$" + f.FieldType + "Field";

请注意,我使用了 Field[0],这是因为在标准模式下的呈现是一次处理一个字段(如上所述),所以当 OnPostRender 事件触发时,ctx.ListSchema 中始终只有一个字段。

一旦我们获得了 HTML 控件,我们就可以订阅它的 onchange 事件,并且每次发生更改时,我们都可以执行一些异步事件,然后根据收到的响应决定应该隐藏哪些颜色。

但是如何隐藏单选按钮呢?

再次进入开发者工具窗口。在这里:

所以基本上,这里的结构相同。但现在,上下文中只有一个字段这一事实对我不利:我不想硬编码字段 ID,因为这很丑!:) 那么我能做什么?

经过一番挖掘,我发现原始 ListSchema 存储在全局变量 window[ctx.FormUniqueId + "FormCtx"].ListSchema 中。很好,问题解决了。

var colorFieldName = "Color";
var colorField = window[ctx.FormUniqueId + "FormCtx"].ListSchema[colorFieldName];
var colorFieldControlId = colorField.Name + "_" + 
colorField.Id + "_$" + colorField.FieldType + "Field" + colorId;

其中 colorId 是 0 = 黑色, 1 = 白色, 2 = 绿色 等等。

所以我们有了初始控件,订阅了 onchange 事件,从外部列表接收了一些数据,获得了颜色字段控件,剩下的就是禁用它们,这很简单。所以这是最终的代码!

SPClientTemplates.TemplateManager.RegisterTemplateOverrides({

    Templates: {
      OnPostRender: function(ctx){
        var colorField = window[ctx.FormUniqueId + "FormCtx"].ListSchema["Color"];
        var colorFieldControlId = colorField.Name + "_" + 
        colorField.Id + "_$RadioButton" + colorField.FieldType + "Field";
        
        var f = ctx.ListSchema.Field[0];
        if (f.Name == "Car")
        {
          var fieldControl = $get(f.Name + "_" + 
          f.Id + "_$" + f.FieldType + "Field");
          
          $addHandler(fieldControl, "change", function(e)
          {
              // first, let's hide all the colors - while the information is loading
              for (var i = 0;i<5;i++)
                $get(colorFieldControlId + i).parentNode.style.display = "none";
          
              var newValue = fieldControl.value;
              var newText = fieldControl[fieldControl.selectedIndex].text;
              
              var context = SP.ClientContext.get_current();
              // here add logic for fetching information from an external list
              // based on newText and newValue
              context.executeQueryAsync(function()
              {
                  // fill this array according to the results of the async request
                  var showColors = [];
                  if (newText == "Kia Soul") showColors = [0, 2, 3];
                  if (newText == "Fiat 500L") showColors = [1, 4];
                  if (newText == "BMW X5") showColors = [0, 1, 2, 3, 4];

                  // now, display the relevant ones
                  for (var i = 0;i<showColors.length;i++)
                    $get(colorFieldControlId + showColors[i]).parentNode.style.display="";
              },
              function(sender, args)
              {
                alert("Error! " + args.get_message());
              });

          });
        } else if (f.Name == "Color") {
          // initialization: hiding all the choices. first user must select a car
          for (var i = 0;i<5;i++)
            $get(colorFieldControlId + i).parentNode.style.display = "none";

        }
      }
    }
});

结果

示例:操作表单布局

有时,有必要添加一些额外的 UI,创建部分,或以其他方式更改表单的布局。在标准模式下,由于简化的 CSR 过程,布局无法像列表视图那样轻松覆盖。但仍然可以进行一些布局操作,这里我演示了一种方法……

在这个例子中,我将在某个字段后的表格中插入一个额外的行。

正如我已经提到的,ListFormWebPart 会渲染特殊的 span 元素,这些元素稍后会在 CSR 过程中被替换。这些 span 元素具有简单的 ID,因此我们可以轻松地操作它们。我们应该在 OnPreRender 回调中进行,这样 span 仍然存在,并且尚未被 field 内容替换。

一旦我们掌握了与特定字段相关的 span 元素,就可以轻松地在其上方或下方插入内容。在下面的示例中,我向表单中插入了一个额外的行。显然,您可以将任何 HTML 放在那里,从而创建任何类型的额外 UI。例如,它可以是一张地图,通过点击它,用户可以预先填充表单值……

所以代码在这里,它很简单。

SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
    OnPreRender: function(ctx) {
      var fieldName = ctx.ListSchema.Field[0].Name;
      if (fieldName == "Title")
      {
          var span = $get(ctx.FormUniqueId + ctx.FormContext.listAttributes.Id + fieldName);
          var tr = document.createElement('tr');
          tr.style.backgroundColor = "#ada";
          tr.innerHTML="<td colspan='2'>&nbsp;</td>";
          var fieldTr = span.parentNode.parentNode;
          fieldTr.parentNode.insertBefore(tr, fieldTr.nextSibling);
      }
    }
  });

结果

自定义模式

自定义模式应该是全功能的 CSR 模式,其中布局、字段标题和字段控件都通过 CSR 生成。

不幸的是,根据我的调查,Custom 模式似乎并没有真正得到测试,因为 Bug 非常明显且随处可见:(

所以,如果您通过 Web 部件属性将列表表单切换到 Custom 模式,会发生以下情况:

令人惊叹,不是吗?:)

我进行了一些调查,经过大约 3 天的斗争,我得出了某种解决方法。但我不得不编写大量的包装代码,即使付出了所有努力,解决方案也并非完美,我不得不通过 OnPreRender 方法执行额外的操作来修复一个小问题;我甚至不得不手动渲染字段标签……

总的来说,我根本不建议切换到 Custom 模式。

结论

列表表单的 CSR 是解决大多数常见列表表单自定义任务的绝佳方式。它远非完美,但可能是最好的方法,也是推荐和支持的方法。因此,在规划表单自定义时,我绝对会坚持使用 CSR。

不幸的是,旨在解决布局自定义的模式在撰写本文时并未修复,因此目前进行布局更改的唯一令人满意的方法是使用 DOM 操作,对于大量的字段来说,这可能相对较慢。

祝您的表单好运!

更新 2015.10.16: 即使在 SP2016 IT Preview 中,CustomLayout 模式问题仍未得到修复,我提出了一个包含 50 行代码的解决方案,解决了布局问题,实现了完全支持 CSR 的自定义模板。请查看。

另外,请查看我很久以前写的另一篇关于将 CSR 与 KnockoutJs 一起使用的文章。

© . All rights reserved.