In my last post an action based teams messaging extension created by Teams Toolkit for Visual studio based on C# and Blazor was described. This time a pendant, using Typescript, created by Teams Toolkit for VSCode code shall be described.
Once more the challenge is to combine the backend bot framework capability with UI components, responsible for the action based task modules. Teams Toolkit for VSCode regularly has a “How to” for that instead of providing a setup for it to build from scratch but even that is quite error prone and a linked sample is the better source. Also a 3rd component will enter stage but let’s come to that a bit later and describe it here step by step.
Content
Setup
Combining two capabilities, of course needs the decision with which one to start. As the messaging extension is the much bigger one it’s a good decision to start with that. The UI components can then be taken from another solution and put into it. Nevertheless, it turned out that both ways provided many challenges, that were starting with the tab or starting with the messaging extension while adding the other one on top.
What is not really mentioned in the How-to’s but essential for the different compilations of backend and front end is the split up of package.json and tsconfig.ts from one source into two targets (/bot and /ui). One package.json is placed in the root while each target (/bot and /ui) has another one combined with a tsconfig. (A 3rd capability, that is the /tab/api backend is not even mentioned)
| package.json | bot\package.json | bot\tsconfig.json | |
| ui\package.json | ui\tsconfig.json | ui\api\… |
Also totally neglected is the debug capability, especially when it comes to two backend solutions (bot and azure function, (refer later in this post or in the linked repository))
Initial Task Module
Based on the fetch=true setting in the teams manifest directly on executing the messaging extension, the bot framework is reached inside the method handleTeamsMessagingExtensionFetchTask.
This method is simply responsible for returning a task module including a url taken from the ui part of this solution.
Add UI Capability
After the UI components are set up the messaging extension needs to know from where to load the initial one (will be done from the bot framework, see above). Therefore, a change in the compose extension needs to be done which points to the right endpoint. And although in this sample only one task module is needed a routing functionality will be left so it can be reactivated at any point of time in case a second task module is needed, too.
But the UI itself is not very complicated. It consists of several FluentUI 9 components (List (Preview!), RadioGroup, Button) and establishes loading and filtering product data.

The magic happens once an item is invoked either by double click or by select and push the button. In that case a dialog.url.submit is called which transfers the selected product to the bot framework where it “arrives” in handleTeamsMessagingExtensionSubmitAction. But also take a note what happens in the list on the change handler. If there is no real change then double click is the result and the same action like on button click is executed.
onSelectionChange={(_: any, data: any) => {
setSelectedItems(data.selectedItems);
if (data.selectedItems[0] === selectedItems[0]) {
// "Double click!": Excecute Button click
btnClicked();
}
const btnClicked = React.useCallback(() => {
dialog.url.submit({product: selectedProduct});
}, [selectedProduct]);

Retrieve Data
Already for the UI selection form products need to be retrieved from the backend database. This will be done by a dedicated client.
const getTableClient = (): TableClient => {
const accountName: string = process.env.AZURE_TABLE_ACCOUNTNAME!;
const storageAccountKey: string = process.env.AZURE_TABLE_KEY!;
const storageUrl = `https://${accountName}.table.core.windows.net/`;
const tableClient = new TableClient(storageUrl, "Products2", new AzureNamedKeyCredential(accountName, storageAccountKey));
return tableClient;
export async function getOrders(category: string) {
const tableClient = getTableClient();
const products: IProduct[] = [];
let productEntities: any;
if (category === '' || category === 'All') {
productEntities = await tableClient.listEntities<IProduct>();
}
else {
productEntities = await tableClient.listEntities<IProduct>({
queryOptions: { filter: odata`Category eq ${category}` }
});
}
let i = 1;
for await (const p of productEntities) {
const product = {
Id: p.partitionKey,
Name: p.rowKey,
Orders: p.Orders as number,
Category: p.Category as string
}
products.push(product);
i++;
}
return products;
But where to place this?
Bot
API Backend
Tab Frontend
In fact, now, there are three capabilities, which also offer three different entry points, especially when called from local debug.
The fact that a basic Teams tap application now already holds the capability of a backend api is totally ignored so far. As it’s already placed inside the tab capability it is better to leave it here than let it collide with the bot. (Especially in local debug environment with mixing bot tunnel and localhost url you will get struggling so I decided to leave duplicate code here). That means when copying the \src from a fresh tap solution the \api folder shall be included and duplicate \azService is established.
For a local debug or running in azure it is necessary to put the following variables pointing to the table inside the configuration (teamsapp.local.yml for local configuration)
AZURE_TABLE_ACCOUNTNAME + AZURE_TABLE_ACCOUNTNAME
On the other hand it later becomes clear that the update process clearly belongs to the bot which will produce some duplicate code. And also AZURE_TABLE_ACCOUNTNAME + AZURE_TABLE_ACCOUNTNAME need to be placed there as well.
Update Data
Similar to retrieving the data, a single product by ordering can be updated. The only difference is that the update is initiated from the bot framework (retrieving actions from adaptive cards). This makes the injection slightly different as mentioned above. The rest stays the same. A service is taken and it first establishes a client and then executes the needed function.
export async function updateOrders(data: Record<string, unknown>) {
const tableClient = getTableClient();
const prodId = new String(data.Id ?? "");
const prodName = new String(data.Name ?? "");
const prodOrders: Number = new Number(data.Orders ?? 0);
const prodAddOrders1: Number = new Number(data.orderId ?? 0);
const prodAddOrders2: Number = new Number(data.orderId2 ?? 0);
const prodAddOrders3: Number = new Number(data.orderId3 ?? 0);
const prodAddOrders4: Number = new Number(data.orderId4 ?? 0);
const prodAddOrders5: Number = new Number(data.orderId5 ?? 0);
const newProduductOders = prodOrders.valueOf() +
prodAddOrders1.valueOf() +
prodAddOrders2.valueOf() +
prodAddOrders3.valueOf() +
prodAddOrders4.valueOf() +
prodAddOrders5.valueOf();
const tableEntity =
{
partitionKey: prodId.toString(),
rowKey: prodName.toString(),
Orders: newProduductOders
};
await tableClient.upsertEntity(tableEntity);
const returnProduct: IProduct = { Id: prodId.toString(), Name: prodName.toString(), Orders: newProduductOders, Category: ''};
return returnProduct
}
What might be confusing here is the more type safe Record<string, unknown> construction. This can be handled fast with classes in that of primitive values.
The rest is no rocket science anymore. After summing up the order values, one tableEntity is created with the new order value and the Id as partitionKey and the Name as rowKey for identification. The tableEntity gets updated and returned to the bot framework where it finally is returned as a display adaptive card.

This was my first attempt to build a solution with teams toolkit for visual studio code. Although it was considered to be the (more, against Visual Studio C#) mature one and my solution was already in place “on the other side” for me it produced a lot of headaches. So it seems to be a long way to go before a bit more complex solutions consisting of several capabilities can be easily set up developed. On the other hand the capabilities taken on its own are pretty straightforward so forgive me that I did not explain too much of a tab or the simple part framework method here.
But if in doubt about the described functionality refer to my GitHub repository holding the whole solution’s code.
|
Markus is a SharePoint architect and technical consultant with focus on latest technology stack in Microsoft 365 Development. He loves SharePoint Framework but also has a passion for Microsoft Graph and Teams Development. He works for Avanade as an expert for Microsoft 365 Dev and is based in Munich. In 2021 he received his first Microsoft MVP award in M365 Development for his continuous community contributions. Although if partially inspired by his daily work opinions are always personal. |


























