【旧文】通软电子班牌 GarnitureControl 与 VisualBlock 原理详解

高中时候写的文章,现在搬过来。

一、关于 GarnitureControl

1. GarnitureControl 的加载与获取

查看 GS.Terminal.GarnitureControl.ControlFinder,注意到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ControlFinder : IDisposable
{
[ImportMany(typeof(IControl))]
public IControl[] Views;
// Token: 0x06000005 RID: 5 RVA: 0x000020DC File Offset: 0x000002DC
public bool Find()
{
try
{
AggregateCatalog aggregateCatalog = new AggregateCatalog();
DirectoryCatalog item = new DirectoryCatalog(AddonActivator.AddonContext.Addon.Location + "\\Controls", "*.dll");
aggregateCatalog.Catalogs.Add(item);
new CompositionContainer(aggregateCatalog, new ExportProvider[0]).ComposeParts(new object[]
{
this
});
return true;
}
catch (Exception errorException)
{
AddonActivator.AddonContext.Logger.Error("查找Control出错", errorException);
}
return false;
}
}

基本操作,没什么好说的,使用 System.ComponentModel.Composition 反射动态地加载程序集中指定类型(实现了 IControl 接口)的类罢了。

看到服务类。注意,这一段比较重要!虽然说只是获取一个 GarnitureControl 对象,但是涉及到一个 Action,其他地方可能用到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
namespace GS.Terminal.GarnitureControl
{
// Token: 0x02000005 RID: 5
public class Service
{
// Token: 0x06000012 RID: 18 RVA: 0x0000225C File Offset: 0x0000045C

// 两个参数。一个键,另一个是返回的 Action
public UserControl FindControlByKey(string key, ref Action<object> handle)
{
GarnitureControl garnitureControl = GarnitureControl.Controls.FirstOrDefault((GarnitureControl ss) => ss.ControlKey == key);
// 根据名称获取
if (garnitureControl == null)
{
return null;
}
UserControl controlEntity = garnitureControl.ControlEntity;
IControl @object = (IControl)controlEntity;
// 强制类型转换为 IControl。因为 @object.setData() 方法实现自 IControl.setData()
handle = new Action<object>(@object.setData);
// 这样,在别处就可以通过 handle(args) 调用 @object.setData() 了
return controlEntity;
}
}
}

2. GarnitureControl 的有关操作

关于如何添加 GarnitureControl 不是这里讨论的重点。这里主要介绍如何操纵已经存在的 GarnitureControl

以跑马灯为例。看到 /GS.Terminal.GarnitureControl/Controls/CommonControls.dll - CommonControls.BannerMessage。我们主要关心它的 setData() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
[ControlInfo("跑马灯信息展示", "BannerMessage")]
// ControlInfo 属性中 BannerMessage 就是 FindControlByKey() 的参数 key
public class BannerMessage : UserControl, IControl, IComponentConnector
{
public void setData(object data)
{
bool flag = data != null;
if (flag)
{
bool flag2 = data.ToString().StartsWith("Command.Add");
// 如果发送的 data 以 Command.Add 开头……
if (flag2)
{
this.MsgList.Add(data.ToString().Substring(11));
// ……则添加 Command.Add 后面的文本到跑马灯列表
}
bool flag3 = data.ToString().StartsWith("Command.Remove");
// 同理
if (flag3)
{
this.MsgList.Remove(data.ToString().Substring(14));
}
bool flag4 = this.MsgList.Count == 1;
// 如果有跑马灯列表里有文本……
if (flag4)
{
// ……则播放
this.canvas1.Visibility = Visibility.Visible;
this.PlayIndex = 0;
this.CeaterAnimation(this.msg);
}
bool flag5 = this.MsgList.Count == 0;
if (flag5)
{
// ……否则隐藏跑马灯组件
this.canvas1.Visibility = Visibility.Collapsed;
this.msg.Text = "";
}
}
}
}

再看看 GS.Terminal.SmartBoard.Logic.Garitures 中如何初始化 BannerMessage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
internal static void InitBannerMessageControl()
{
double width;
double left;
double top;
if (SystemParameters.PrimaryScreenWidth == 1080.0)
{
width = 1020.0;
left = 30.0;
top = 1598.0;
}
else
{
width = 1700.0;
left = 110.0;
top = 840.0;
}
// 设置位置,无需多言
GarnitureControl garnitureControl = new GarnitureControl();
Action<object> controlHandle = null;
// 实例化
UserControl userControl = Utilites.FindControlByKey("BannerMessage", ref controlHandle);
// 通过 Utilities 中对 GS.Terminal.GarnitureControl.Service.FindControlByKey() 方法的封装,查询名为 BannerMessage 的 GarnitureControl
garnitureControl.ControlHandle = controlHandle;
// 设置其 ControlHandle,相当于 setData()
userControl.Width = width;
garnitureControl.ControlEntity = userControl;
garnitureControl.ID = Utilites.AddGarnitureControl(userControl, top, left);
// 添加
garnitureControl.Key = "BannerMessage";
GaritureCore.GarnitureControlList.Add(garnitureControl);
Program.TerminalStateManagement.StateChanged += BannerMessageControl.TerminalStateManagement_StateChanged;
}

这时候,看到 GS.Terminal.SmartBoard.Logic.Garitures.BannerMessageControl,它实际上是对 CommonControls.BannerMessage.setData() 的一系列封装。

以添加跑马灯消息为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
internal static void AddBannerMsg(string Msg)
{
if (BannerMessageControl.banner == null)
{
BannerMessageControl.banner = GaritureCore.GarnitureControlList.FirstOrDefault((GarnitureControl ss) => ss.Key == "BannerMessage");
}
if (BannerMessageControl.banner != null)
{
Program.AddonContext.Logger.Debug("AddBannerMsg", null);
DispatcherHelper.CheckBeginInvokeOnUI(delegate
{
BannerMessageControl.banner.ControlHandle("Command.Add" + Msg);
// 和 setData() 完全对应
// 相当于 BannerMessage.setData("Command.Add" + Msg)
});
}
}

同时,这里再提出一种不需要修改班牌 dll 的自定义跑马灯的方法。

可以通过 GarnitureControl 插件服务类中的 FindControlByKey() 获取 ControlHandleAction<object> 实例的引用(注:这时候也会返回一个 UserControl 类型的实例的引用,但是那个实例的引用是无法操纵的,这涉及到 UI 线程安全等等,不是讨论的重点),然后就可以控制 GarnitureControl 的行为了。

二、关于 VisualBlock

0. 如何打开 localData.db

要理解以下代码,需要查看 localData.db 的内容。
考虑使用 SqliteStudio,加密算法 System.Data.Sqlite: RC4,密码 123

1. VisualBlock 的加载与初始化

查看 GS.Terminal.SmartBoard.Logic.VisualBlockCore.Startup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// 此方法由于反编译出现了一定程度的混乱,但是不影响理解。
internal void LoadVisualTemplate()
{
try
{
using (Session session = Program.ObjectSpace.GetSession(ChannelMode.Multiton))
// 准备进行数据库操作
// 下面,以 Local_ 开头的 VisualPublish,VisualTemplate 等为数据库数据类型,
// 不含此开头的为用于 UI 显示的,根据数据库数据类型创建的实例
{
this.Templates.Clear();
this._ThemeName = string.Empty;
object dblocker = this._dblocker;
// 线程锁,防止多线程读写数据库时发生冲突
lock (dblocker)
{
Local_Visual_Publish local_Visual_Publish = session.Query<Local_Visual_Publish>()
.FirstOrDefault((Local_Visual_Publish ss) => ss.TerminalID == Program.MachineId);
// 在数据库的 Local_Visual_Publish 表中,查询 TerminalID 键为此机器的 MachineID 的项
// 相当于 select * from Local_Visual_Publish where TerminalID = ...
// Visual_Publish 是一些基础的设置,比如默认的主题

if (local_Visual_Publish != null)
{
this._ThemeName = local_Visual_Publish.ThemeName;
// 将班牌的主题设为查询到的 Local_Visual_Publish 的 ThemeName 键的值

List<Local_VisualTemplate> templates = (from ss in local_Visual_Publish.Templates
orderby ss.SortIndex
select ss).ToList<Local_VisualTemplate>();
// 这里需要指出两点:
// 1. 为什么没有再单独查询 VisualTemplate?因为查询 VisualPublish 时,
// 已经通过 GS.Unitive.Framework.Data.Xpo 相关内容自动获取了
// 其 Publish 值应当与 MachineID 对应,参见本地数据库 localData.db 内容
// 2. orderby 只是一个查询用的子句,这里进行了一个根据 SortIndex 从低到高的排序

int num = 1;
Func<Local_VisualBlock, VisualBlockItem> <>9__2;
foreach (Local_VisualTemplate local_VisualTemplate in templates)
{
// 遍历刚刚查询到的 Local_VisualTemplate

List<BlockTemplate> templates2 = this.Templates;
// 用来存储构造完毕的 BlockTemplate 的列表
// 一个 BlockTemplate 相当于是一个滑动页面

BlockTemplate blockTemplate = new BlockTemplate();
// 创建新的 BlockTemplate 实例

blockTemplate.TemplateName = local_VisualTemplate.TemplateName;
// 模板内部名称

blockTemplate.DisplayName = local_VisualTemplate.DisplayName;
// 用于显示的名称(上方导航栏中显示,参见 GS.Terminal.SmartBoard.Logic.Garitures.ViewTabBarControl)

blockTemplate.Index = num++;
// 索引

blockTemplate.TemplateType = BlockTemplateType.Theme;
// 主题

IEnumerable<Local_VisualBlock> source = local_VisualTemplate.VisualBlocks.ToList<Local_VisualBlock>();
// 获得该 VisualTemplate 下所有的 Local_VisualBlock 项目,准备遍历
// 注意,这里获取到的 Local_VisualBlock 的 Template 键的值应该与 Local_VisualTemplate 的 TemplateID 对应
// 参见数据库内容

Func<Local_VisualBlock, VisualBlockItem> selector;
// 选择器,根据 Local_VisualBlock 创建相应的 VisualBlockItem

if ((selector = <>9__2) == null)
{
selector = (<>9__2 = delegate(Local_VisualBlock b)
{
BaseBlock blockEntity = null;
DispatcherHelper.RunAsync(delegate
{
blockEntity = this.GetBlock(b.BlockTypeName);
// 获取相应的 Block
// 这里是一个对 GS.Terminal.VisualBlock.Service.GetBlock(string key) 的封装
// 获取 /GS.Terminal.VisualBlock/Bundles/ 下 .dll 中
// 含有属性 [ExportMetadata("Name", <key>)] ,且继承了 BaseBlock 类的各种 Block

blockEntity.DataSource = (b.DataSource.StartsWith("http") ? b.DataSource : (Program.WebPath + "/" + b.DataSource));
blockEntity.Init(Program.AddonContext);
// 初始化 Block,设置远程数据 api 等
}).Wait();
if (!string.IsNullOrEmpty(b.NavTemplateName))
{
blockEntity.NavPageIndex = templates.FindIndex((Local_VisualTemplate ss) => ss.TemplateName == b.NavTemplateName) + 1;
}
return new VisualBlockItem
{
Id = Guid.Parse(b.BlockID),
// ID

// 从这里开始的几项都可以在数据库的 Local_VisualBlock 表中找到
BlockComponent = b.BlockComponent,
// block 的内容,相当于一个描述,没有实际作用

BlockTypeName = b.BlockTypeName,
// block 类型名称
DataSource = b.DataSource,
// 远程 api
NavTemplateName = b.NavTemplateName,
// 导航栏名称等等

Height = b.Height,
Width = b.Width,
X = b.X,
Y = b.Y,
// 位置
// 数据库中包含的键值结束
DataContext = blockEntity
// block 的内容
};
});
}
blockTemplate.Blocks = source.Select(selector).ToList<VisualBlockItem>();
// 将生成了的 Block 全部添加到 blockTemplate
templates2.Add(blockTemplate);
// 添加生成了的 blockTemplate 到所有 BlockTemplate 的列表
}
}
}
DataUpdate.Instance.RebuildUpdateList();
// 重新生成数据刷新列表
session.Disconnect();
// 断开数据库连接
}
}
catch (Exception errorException)
{
Program.AddonContext.Logger.Error("加载主题模版异常", errorException);
}
}

2. VisualBlock 如何获取数据?

在刚刚的加载过程中,RebuildUpdateList() 方法创建了一个列表,其内容为依据实例化了的各种 VisualBlock 创建的一系列 UpdateBlock,用于管理 VisualBlock 数据的获取和更新。

不过注意,UpdateBlock 类只是描述了一个数据结构,实际的逻辑并不在此处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class UpdateBlock
{
public UpdateBlock(VisualBlockItem item)
{
this.BlockEntity = (IUpdate)item.DataContext;
// 相当于 Block 的内容
// 这里强制类型转换为 IUpdate 是为了在后面使用这个 Block 实现的 IUpdate 接口的 LoadLocalData() 方法
this.BlockId = item.Id;
this.TypeName = item.BlockTypeName;
this.WebRequester = new VisualBlockWebRequest(new Uri(Program.localSetting.GlobalConfig.WebPath + "/" + item.DataSource),
string.Format("{0}_{1}.json", item.BlockTypeName, item.Id));
// 指定了获取数据的 api 和数据存储路径
// 班牌目录下 /cache/BlockCache/ 下的 json 文件就是这么来的
}
...
}

下面展示的两个方法控制 Block 数据的更新。其中,UpdateAllBlock() 方法会在 GS.Terminal.SmartBoard.Logic 插件启动时被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// GS.Terminal.SmartBoard.Logic.VisualBlockCore.DataUpdate

// Token: 0x06000256 RID: 598 RVA: 0x0000BB54 File Offset: 0x00009D54
internal void UpdateAllBlock()
{
foreach (string typeName in this._updateBlocks.Keys)
{
this.UpdateBlockDataByTypeName(typeName);
}
}

// Token: 0x06000257 RID: 599 RVA: 0x0000BBAC File Offset: 0x00009DAC
internal void UpdateBlockDataByTypeName(string typeName)
{
List<UpdateBlock> list;
if (this._updateBlocks.TryGetValue(typeName, out list))
{
list.ForEach(delegate(UpdateBlock b)
{
// 遍历 UpdateBlock
if (!b.IsBusy)
{
try
{
b.IsBusy = true;
if (b.WebRequester.UpdateData() == VisualBlockUpdateResult.Update)
// 先从远程 api 获取数据到本地的 json 文件
{
b.BlockEntity.LoadLocalData(b.WebRequester.CacheFile);
// 调用该 Block 实现的 IUpdate 的 LoadLocalData() 方法
// 注意!类似的写法可以用于人为修改 Block 的内容!
// 比如在“班级风采”播放自定义视频等等

// 此外,每个 VisualBlock 的 json 数据结构都各有差别,但基本都包含一个 result 节点
// 有些 VisualBlock 从服务器获取到的 json 数据结构的 result 节点的内容是经过 gzip 压缩的
// 比如 RichNotice 等
}
}
finally
{
b.IsBusy = false;
}
}
});
}
}

这些逻辑就是实现班牌滑动页面上控件更新的全部了。

仿照这样的逻辑,我们可以写一个人为创建自定义 VisualTemplate 的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public void RewriteVisualTemplate(string template_name, string display_name, int template_overwrite_index, List<VisualBlockItem> items)
{
BlockTemplate blockTemplate = new BlockTemplate();
blockTemplate.TemplateName = template_name;
blockTemplate.DisplayName = display_name;
blockTemplate.Index = template_overwrite_index;
blockTemplate.TemplateType = BlockTemplateType.Theme;
// enum BlockTemplateType.Theme = 0x0

blockTemplate.Blocks = items;
// 这里需要指出的是,其实 BlockTemplate 的 List<VisualBlockItem> Blocks 没有在 BlockTemplate 的构造函数中实例化,需要手动实例化
// 此外,一个 BlockTemplate 可能包含多个 VisualBlockItem,比如首页

blockTemplate.Previous = Utilites.ViewModelLocator.MainPage.TemplateList[template_overwrite_index - 1];
blockTemplate.Next = Utilites.ViewModelLocator.MainPage.TemplateList[template_overwrite_index + 1];

// 滑动页面的切换实际上用的是一个类似链表的结构,所以可以这样替换
DispatcherHelper.RunAsync(
() =>
{
// DispatcherHelper 为 Mvvm 的内容
// (GS.Terminal.SmartBoard.Logic.Core.)Utilites 为 GS 的杂项工具
Utilites.ViewModelLocator.MainPage.TemplateList[template_overwrite_index] = blockTemplate;
// 执行替换
}
);
}

// 创建 VisualBlockItem
// 参数与 localData.db 中的键对应
public static VisualBlockItem GetBlock(string block_name, string json_filename, string component, int width, int height, int x, int y, string data_source = "", string nav_template_name = "")
{

IBlockService firstOrDefaultService = Program.AddonContext. GetFirstOrDefaultService<IBlockService>("GS.Terminal.VisualBlock");
// 获取 VisualBlock 有关服务
BaseBlock block = firstOrDefaultService.GetBlock(block_name);
block.Init(Program.AddonContext);

IUpdate update = (IUpdate)block;
update.LoadLocalData(media_json_filename);


return new VisualBlockItem
{
Id = Guid.NewGuid(),
BlockComponent = component,
BlockTypeName = ((IBlock)block).TypeName,
DataSource = data_source,
NavTemplateName = nav_template_name,
Width = width,
Height = height,
X = x,
Y = y,
DataContext = (BaseBlock)update
}
}

3. 一种可能的不需要修改 dll 的添加自定义 VisualBlock 的方法

另外,这里再提出一种不需要修改 dll 的方法。

使用 GS.Terminal.LogicShellIViewHelperService 或者 GalaSoft.MvvmLightSimpleIoc 获取相关的 ViewModel,再进行操作。


【旧文】通软电子班牌 GarnitureControl 与 VisualBlock 原理详解
https://lizi.moe/2025/12/05/【旧文】通软电子班牌-GarnitureControl-与-VisualBlock-原理详解/
作者
李萌
发布于
2025年12月5日
许可协议