iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 13
0
Software Development

自己用的工具自己做! 30天玩轉VS Code Extension之旅系列 第 13

Day13 | 打造VSCode上的TreeView樹狀選單 (二) - DataProvider的原理與相關觀念 X List的增刪改查

大家好,我是韋恩,今天是第十二天,今天我們會介紹DataProvider的原理與相關觀念,並實際練習新增、修改與刪除TreeDataProvider的樹狀選單上的選項。

DataProvider設計解析


在剛才的範例裡,我們已經成功了register樹狀選單的Data Provider,接下來,我們需要能夠新增選單的選項,並能夠通知VSCode刷新介面。為此,我們需要VSCode的事件發射器EventEmitter。

那什麼是EventEmitter呢?簡單來說,EventEmitter是一個實做了觀察者模式的物件。在VSCode裡,vscode.EventEmitter提供了一個fire方法做事件發射與通知,同時提供一個event方法讓想接收事件的人訂閱、監聽事件的消息。

vscode.EventEmitter的用法如下:

/** 
 * 創建新的EventEmitter物件
 */
const eventEmitter = new vscode.EventEmitter();

/** 
 * 發射事件相關資料
 */
eventEmitter.fire('new changed data!');

/** 
 * 監聽與事件相關資料
 */
eventEmitter.event(message => {
	console.log(`Receive message: ${message}`); // 'Receive message: new changed data!'
});

透過上例,我們可以看到EventEmitter提供了兩個方法,fire(發送事件源資料)event(監聽、接收事件源資料)。這是為什麼這樣區分呢?

藉由emitter物件的方法,我們可以徹底的將事件發生的源頭事件對應的處理方式切分開來。

在VSCode裡,複雜的元件,透過TreeDataProvider提供event的方法監聽資料改變的事件,因此我們的DataProvider會將事件監聽的方法暴露出去,如下所示:

class DataProvider implements vscode.TreeDataProvider<TreeViewItem> {

	private eventEmitter = new vscode.EventEmitter<TreeViewItem | undefined | void>();

	public get onDidChangeTreeData(): vscode.Event<TreeViewItem | undefined | void> {
		return this.eventEmitter.event;
	}
    ...
}

我們可以看到,onDidChangeTreeData只會回傳eventEmitter.event,而不會像上上面的用法直接收聽event,這樣的用法保證了DataProvider可以被外部監聽事件。所以這個暴露出去onDidChangeTreeData方法,其實就如同我們昨天提到的創建類元件的監聽用法。

在昨天我們提到,創建類元件是可以被收聽資料改變的事件的。當我們create一個元件,我們可以使用元件裡的監聽方法,如createInputBox

const inputBox = vscode.window.createInputBox();

inputBox.onDidChangeValue((message: string) => {
	...
})

onDidChangeValue可以被外部監聽多次,也可以被多個對象監聽。

現在我們提供的onDidChangeTreeData,沒有什麼不同,它一樣可以保證DataProvider可以被外部監聽事件,監聽多次,也可以被多個對象監聽,只是這次我們改為讓VSCode內部自己決定怎麼使用這個監聽方法,如下所示:

const dataProvider = new DataProvider();

dataProvider.onDidChangeTreeData((data: TreeViewItem | undefined | void) => {
    const treeItemsData = dataProvider.getChildren();
    ...
});

從上面的範例我們很容易了解,DataProvider只會負責提供onDidChangeTreeData監聽事件,被註冊後,VSCode會自己監聽TreeData的資料,並且決定要怎麼處理資料與刷新VSCode視圖的UI元件,無需開發者費神。

與之對應的,dataProvider也應該提供事件通知的方法讓外部可以傳送新的資料,並讓VSCode接收到,這就是我們開發者要手動操作的方法了,底下我們會透過實作新增與刪除資料的實例來練習這部分。

註:跟Nodejs內建的EventEmitter或其他程式框架常見的EventBus比起來,vscode.EventEmitter的用法簡單與簡化許多。通常在其他library的實作裡,一個eventEmitter可以監聽多個事件,同時在傳送事件源頭的資料時指定其中一個事件發射資料,但在vscode.EventEmitter裡,emitter並不會監聽多個事件,而僅會將不同事件源頭的資料發佈給訂閱者,這是專為VSCode的元件特化的設計。

Day12練習:新增與刪除TreeView的選項吧!


好的,我們又了解了不少觀念與知識呢!讓我們繼續回到實作吧! 練習,將理論落地,是一把真正掌握一門知識與技藝的鑰匙。讓我們開始為使用者提供新增TreeView資料與刪除資料的功能吧!


環境準備

  • 使用yoman產生extension專案:
yo code
  • 依序輸入專案名稱、id等資訊

專案Contribution Points命令配置:
  • 註冊ViewContainer下ActivityBar的Explorer與TreeView

讓我們照以下指示設定extension專案,在packagage.json,我們要新增一個ViewContainer,用於註冊VSCode左邊的activitybar,並指定一個新的explorer。之後,我們在Contribution Points裡的View裡使用剛註冊完的explorer-id底下註冊TreeView跟相關Command,如下所示:

{
    ...
   "contributes": {
		"viewsContainers": {
				"activitybar": [
						{
								"id": "treeview-explorer",
								"title": "TreeView Explorer",
								"icon": "assets/output.svg"
						}
				]
		},
		"views": {
				"treeview-explorer": [
						{
								"id": "treeview",
								"name": "TreeView Explorer",
								"icon": "assets/output.svg",
								"contextualTitle": "TreeView Explorer"
						}
				]
        },
		"viewsWelcome": [
			{
					"view": "treeview",
					"contents": "Welcome to NewTreeView \n [learn more](https://code.visualstudio.com/api/extension-guides/tree-view/).\n[Resgister Data Provider](command:day13-dataprovider.registerDataProvider)",
			}
        ],
        ...
	},
    ...
}

註冊完上面配置後可以在VSCode的左側看到左下的activityBar已經註冊了TreeView Explorer,點擊可以展開我們自訂的Explorer。

  • 註冊新增、刪除等Command
{
    ...
   "contributes": {
        ...
		"commands": [
			{
				"command": "day13-dataprovider.registerDataProvider",
				"title": "Day13: Register DataProvider"
			},
			{
				"command": "day13-dataprovider.01_add",
				"title": "Day13: Add New Item"
			},
			{
				"command": "day13-dataprovider.02_edit",
				"title": "Day13: Edit Exist Item",
				"icon": {
					"light": "assets/edit.svg",
					"dark": "assets/edit.svg"
				}
			},
			{
				"command": "day13-dataprovider.03_delete",
				"title": "Day13: Delete Item",
				"icon": {
					"light": "assets/trash.svg",
					"dark": "assets/trash.svg"
				}
			}
		]
        ...
	},
    ...
}
  • 創建TreeView Explorer上綁定新增、修改、刪除命令的選單

{
    ...
   "contributes": {
        ...
		"menus": {
			"view/title": [
					{
							"command": "day13-dataprovider.01_add",
							"when": "view == treeview"
					}
			],
			"view/item/context": [
					{
							"command": "day13-dataprovider.02_edit",
							"when": "view == treeview && viewItem == treeviewitem",
							"group": "inline"
					},
					{
						"command": "day13-dataprovider.03_delete",
						"when": "view == treeview && viewItem == treeviewitem",
						"group": "inline"
				 },
					{
							"command": "day13-dataprovider.03_delete",
							"when": "view == treeview && viewItem == treeviewitem"
					}
			]
		}
        ...
	},
    ...
}
  • TreeViewDataProvider實作(treeview-data-provider.ts)

import * as vscode from 'vscode';

export class TreeViewItem extends vscode.TreeItem {
	constructor(label: string, collapsibleState?: vscode.TreeItemCollapsibleState) {
		super(label, collapsibleState);
		this.contextValue = 'treeviewitem';
	}
}

export class DataProvider implements vscode.TreeDataProvider<TreeViewItem> {

	private dataStorage = [
		new TreeViewItem('TreeItem-01'),
		new TreeViewItem('TreeItem-02'),
		new TreeViewItem('TreeItem-03'),
	];

	private eventEmitter = new vscode.EventEmitter<TreeViewItem | undefined | void>();

	public get onDidChangeTreeData(): vscode.Event<TreeViewItem | undefined | void> {
		return this.eventEmitter.event;
	}

	public getTreeItem(element: TreeViewItem): vscode.TreeItem | Thenable<vscode.TreeItem> {
		return element;
	}
  
	public getChildren(element?: TreeViewItem): vscode.ProviderResult<TreeViewItem[]> {
		return Promise.resolve(this.dataStorage);
	}

    public addItem(newItem: TreeViewItem) {
		this.dataStorage.push(newItem);
		this.updateView();
	}

	public editItem(item: TreeViewItem, name: string) {
		const editItem = this.dataStorage.find(i => i.label === item.label);
		if (editItem) {
			editItem.label = name;
			this.updateView();
		}
	}

	public deleteItem(item: TreeViewItem) {
		this.dataStorage = this.dataStorage.filter(i => i.label !== item.label);
		this.updateView();
	}

	private updateView() {
		this.eventEmitter.fire();
	}
}

  • extension.ts dataProvider與命令邏輯設計
import * as vscode from 'vscode';

import { DataProvider, TreeViewItem } from './treeview-data-provider';

export function activate(context: vscode.ExtensionContext) {

	const dataProvider = new DataProvider();

	let initView = vscode.commands.registerCommand('day13-dataprovider.registerDataProvider', () => {
	
		vscode.window.registerTreeDataProvider('treeview', dataProvider);

		vscode.window.showInformationMessage('Create day13-treeview!');
	});

	let addItem = vscode.commands.registerCommand('day13-dataprovider.01_add', async () => {

		const itemId = await vscode.window.showInputBox({
			placeHolder: 'Your New TreeItem Id'
		}) || '';

		if (itemId !== '') {
			dataProvider.addItem(new TreeViewItem(itemId));
		}
            
		vscode.window.showInformationMessage('Add Day-13 TreeViewItem!');

	});

	let editItem = vscode.commands.registerCommand('day13-dataprovider.02_edit', async (item: TreeViewItem) => {
			
        const itemName = await vscode.window.showInputBox({
            placeHolder: 'Your New TreeItem Name'
        }) || '';
			
		if (itemName !== '') {
            dataProvider.editItem(item, itemName);
        }
	
        vscode.window.showInformationMessage('Edit Day-13 TreeViewItem!');
	});

    let deleteItem = vscode.commands.registerCommand('day13-dataprovider.03_delete', async (item: TreeViewItem) => {

	const confirm = await vscode.window.showQuickPick(['delete', 'canel'], {
		placeHolder: 'Do you want to delete item?'
	});

	if (confirm === 'delete') {
		dataProvider.deleteItem(item);
	}

	vscode.window.showInformationMessage('Delete Day-13 TreeViewItem!');
});

	context.subscriptions.push(initView, addItem, editItem, deleteItem);
}

export function deactivate() {}

結語


好啦,今天,我們創建了解了TreeView的DataProvider設計方式,並了解TreeView的新增、刪除等使用方法。

明天會繼續VSCode元件的介紹與實際練習,我們明天見,謝謝大家。

本日參考文件



上一篇
Day12 | 打造VSCode上的TreeView樹狀選單 (一) - 元件基本用法介紹
下一篇
Day14 | OutputChannel,輸出Extension的訊息
系列文
自己用的工具自己做! 30天玩轉VS Code Extension之旅36

尚未有邦友留言

立即登入留言