[]
        
立即下载
(Showing Draft Content)

使用无头浏览器实现设计时预览

此特性为V10.0.0.0新增的特性。

原理

在V10之前的版本,如果单元格要在设计器中实现设计时预览,必须使用WPF技术。这样一个单元格的画法就需要用两种不同的技术实现两遍(JavaScript,WPF)。这给单元格的插件开发带来了很大的技术负担。很多情况下,单元格插件的开发者会使用一个静态图片占位的方式来实现设计时预览。这又给插件的使用者带来了困难。插件的使用者不得不在调整设置之后,反复运行去查看结果,降低了开发效率。

在V10版本,活字格提供了新的设计时预览机制。活字格设计器会启动一个无头浏览器(Chromium),使用运行时的JavaScript代码渲染插件单元格,再把渲染的结果以图片的方式传回活字格设计器。这样,活字格设计器中就可以渲染和运行时几乎一样的单元格UI了。

准备代码

以下代码,实现了一个简单的列表单元格。支持设置列表的标题和内容。

设计时代码:

    public class MyPluginCellType : CellType
    {
        [FormulaProperty]
        [DisplayName("标题")]
        public object Title { get; set; }

        [FlatListProperty]
        [DisplayName("内容")]
        public List<MyObj> Items { get; set; }
    }

    public class MyObj : ObjectPropertyBase
    {
        [FormulaProperty]
        [DisplayName("项目")]
        [ListPropertyItemSetting(DefaultName = "项目", DefaultWidth = 200)]
        public object Name { get; set; }
    }

运行时代码:

class MyPluginCellType extends Forguncy.Plugin.CellTypeBase {
    createContent() {
        this.content = $("<div style='width:100%;height:100%;'></div>");
        this.title = $("<h3/>");
        this.items = $("<ul/>");
        this.content.append(this.title);
        this.content.append(this.items);
        return this.content;
    }
    onPageLoaded() {
        this.onFormulaResultChanged(this.CellElement.CellType.Title, result => {
            this.title.text(result)
        });
        for (const item of this.CellElement.CellType.Items) {
            const itemDom = $("<li>");
            this.items.append(itemDom);
            this.onFormulaResultChanged(item.Name, result => {
                itemDom.text(result)
            });
        }
    }
}
Forguncy.Plugin.CellTypeHelper.registerCellType("MyPlugin.MyPluginCellType, MyPlugin", MyPluginCellType);

设计器中的设置:

image

运行时效果:

image

到此,通过之前章节的内容,可以轻松实现一个插件单元格,但是有一个问题:可以看到,在设计器中,单元格区域是一片空白,不符合活字格所见即所得的设计原则。

基本实现

通过添加以下代码,可以声明单元格以无头浏览器的方式实现设计时预览。

    [Designer(typeof(MyPluginCellTypeDesigner))]
    public class MyPluginCellType : CellType
    {
        //代码不变,使用上例中相同代码,注意类型上标注的Designer......
    }

    public class MyPluginCellTypeDesigner : CellTypeDesigner<MyPluginCellType>
    {
        public override FrameworkElement GetDrawingControl(ICellInfo cellInfo, IDrawingHelper drawingHelper)
        {
            return drawingHelper.GetHeadlessBrowserPreviewControl(); // 使用无头浏览器渲染设计时预览
        }
    }

设计器中的效果:

parameter1

可以看到,简单的几行代码,就可以得到几乎和运行时一模一样的预览效果了。

更精细的控制

区分设计时预览与运行时逻辑

在以上例子中,可以看到,设计时预览和运行时效果几乎是一摸一样的。但是有些情况下,可能需要设计时预览和运行时结果有一些差别。例如上例中,如果把单元格拖拽到设计器中,由于初始的标题和内容都是空,导致单元格里仍然是一片空白。这种情况下,希望能显示一段提示信息,如“请设置标题和内容”。但是这个信息又不希望影响运行时,此时可以通过isDesignerPreview 属性,在前端代码中判断现在的渲染是预览状态还是运行时状态。

示例代码如下:

/// <reference path="../Declarations/forguncy.d.ts" />
/// <reference path="../Declarations/forguncy.Plugin.d.ts" />

class MyPluginCellType extends Forguncy.Plugin.CellTypeBase {
    createContent() {
        this.content = $("<div style='width:100%;height:100%;'></div>");
        if (this.isDesignerPreview &&
            !this.CellElement.CellType.Title &&
            !this.CellElement.CellType.Items?.length) {
            this.content.append($("<div style='color:gray'>请设置标题和内容</div>"));
        }
        this.title = $("<h3/>");
        this.items = $("<ul/>");
        this.content.append(this.title);
        this.content.append(this.items);

        return this.content;
    }
    onPageLoaded() {
        this.onFormulaResultChanged(this.CellElement.CellType.Title, result => {
            this.title.text(result)
        });
        for (const item of this.CellElement.CellType.Items) {
            const itemDom = $("<li>");
            this.items.append(itemDom);
            this.onFormulaResultChanged(item.Name, result => {
                itemDom.text(result)
            });
        }
    }
}
Forguncy.Plugin.CellTypeHelper.registerCellType("MyPlugin.MyPluginCellType, MyPlugin", MyPluginCellType);

设计器效果:

image

预览绑定数据源数据

上例中,内容的设置是设计时手工输入的,如果内容需要从数据库中获取。需要设计器给无头浏览器提供数据

单元格改造如下:

    [Designer(typeof(MyPluginCellTypeDesigner))]
    public class MyPluginCellType : CellType
    {
        [FormulaProperty]
        [DisplayName("标题")]
        public object Title { get; set; }

        [BindingDataSourceProperty(Columns = "Name:项目")]
        [DisplayName("内容")]
        public IBindingDataSource Items { get; set; }
    }

    public class MyPluginCellTypeDesigner : CellTypeDesigner<MyPluginCellType>
    {
        public override FrameworkElement GetDrawingControl(ICellInfo cellInfo, IDrawingHelper drawingHelper)
        {
            return drawingHelper.GetHeadlessBrowserPreviewControl(new GetHeadlessBrowserPreviewControlOptions()
            {
                GenerateCustomArgsAsync = async () =>
                {
                    var data = await drawingHelper.GetDataByBindingDataSourceAsync(CellType.Items);
                    return new object[] { data };
                }
            });
        }
    }

代码说明:

Items 属性修改为数据源属性。GetHeadlessBrowserPreviewControl 方法添加了 GetHeadlessBrowserPreviewControlOptions 参数,并在参数中使用 GetDataByBindingDataSourceAsync 方法获取数据库数据,在通过参数把数据发给无头浏览器。

前端代码修改如下:

class MyPluginCellType extends Forguncy.Plugin.CellTypeBase {
    createContent() {
        this.content = $("<div style='width:100%;height:100%;'></div>");
        this.title = $("<h3/>");
        this.items = $("<ul/>");
        this.content.append(this.title);
        this.content.append(this.items);

        return this.content;
    }
    onPageLoaded() {
        this.onFormulaResultChanged(this.CellElement.CellType.Title, result => {
            this.title.text(result)
        });
        const loadData = (data) => {
            for (const item of data) {
                const itemDom = $("<li>");
                this.items.append(itemDom);
                this.onFormulaResultChanged(item.Name, result => {
                    itemDom.text(result)
                });
            }
        }

        if (this.isDesignerPreview) {
            loadData(this.designerPreviewCustomArgs[0])
        }
        else {
            this.getBindingDataSourceValue(this.CellElement.CellType.Items, null, data => {
                loadData(data);
            }, true);
        }
    }
}
Forguncy.Plugin.CellTypeHelper.registerCellType("MyPlugin.MyPluginCellType, MyPlugin", MyPluginCellType);

通过 this.isDesignerPreview 判断是否为设计时预览,如果是设计时预览,则通过 designerPreviewCustomArgs 属性获取设计时参数(参数中为设计器中获取的数据表数据);不为设计时预览时,通过 getBindingDataSourceValue 方法获取数据。

这样,即可预览数据库中的数据了。

image

通过GetDataByBindingDataSourceAsync方法获取数据表数据,出于性能考虑,默认最大获取行数为 2000行,如果要修改,可以通过第二个参数来设置:

await drawingHelper.GetDataByBindingDataSourceAsync(CellType.Items, new GetDataByBindingDataSourceOptions() { MaxQueryRows = 100 });
控制设计时预览刷新

默认情况下,任何属性变化都会触发单元格重绘。如果单元格有一些属性不会影响预览(如命令,行为控制等),修改时重绘会有不必要性能损失。可以通过设置 EffectPreviewPropertyNames 属性,声明属性列表。此时,只有在属性列表中的属性被修改才会触发重绘。

public class MyPluginCellTypeDesigner : CellTypeDesigner<MyPluginCellType>
{
    public override FrameworkElement GetDrawingControl(ICellInfo cellInfo, IDrawingHelper drawingHelper)
    {
        return drawingHelper.GetHeadlessBrowserPreviewControl(new GetHeadlessBrowserPreviewControlOptions()
        {
            EffectPreviewPropertyNames = new string[]
            {
                    nameof(MyPluginCellType.Title),
                    nameof(MyPluginCellType.Items)
            }
        });
    }
}

同时,设计时预览会有缓存逻辑。影响属性的值发生变化,或者单元格大小发生变化时,会刷新缓存。如果在上述设置都没有发生变化,但是设计时预览需要刷新时(如数据源发生了变化),可以给单元格标注 SupportRefreshPreviewAttribute,来添加刷新设计时预览菜单项。

    [Designer(typeof(MyPluginCellTypeDesigner))]
    [SupportRefreshPreview]
    public class MyPluginCellType : CellType
    {
        [FormulaProperty]
        [DisplayName("标题")]
        public object Title { get; set; }

        [BindingDataSourceProperty(Columns = "Name:项目")]
        [DisplayName("内容")]
        public IBindingDataSource Items { get; set; }
    }

设计器效果:

image