This example adds a Copilot-inspired chat window (DevExpress DxAIChat
component) to a DevExpress-powered Blazor application (using both the DevExpress Report Viewer and Blazor Grid component). Our chat implementation utilizes Azure OpenAI Assistant to answer user questions based on information displayed in the report and/or data grid.
To integrate AI-powered chat capabilities to your next great Blazor application, please follow the steps below:
- Register AI Services within the application.
- Add the DevExpress Chat component (
DxAIChat
). - Export component data and pass it to the AI Assistant.
The following DevExpress Blazor Components were used in this sample project:
- DevExpress Blazor Grid
Our Blazor Grid component displays project management data. You can use the AI Assistant to:- Prioritize tasks.
- Count tasks using status data.
- Filter tasks by owner or priority.
Implementation details: Use an AI Assistant with the DevExpress Blazor Grid.
- Report Viewer
Our Report Viewer includes several reports bound to different data. Use the AI Assistant to interact with report data as follows:- Drill-Down Report: Analyze invoice totals, delivery status, and averages.
- Market Share Report: Compare market share changes across regions.
- Restaurant Menu: Examine price range and categorize vegetarian options.
Implementation details: Add an AI Assistant to the DevExpress Blazor Report Viewer.
[!NOTE]
Open AI Assistant initialization may take time.DxAIChat
is ready for use once Microsoft Azure OpenAI completes its source document scan.
Implementation Details
Register AI Services
[!NOTE]
DevExpress AI-powered extensions follow the "bring your own key" principle. DevExpress does not offer a REST API and does not ship any built-in LLMs/SLMs. You need an active Azure/Open AI subscription to obtain the REST API endpoint, key, and model deployment name. These variables must be specified at application startup to register AI clients and enable DevExpress AI-powered Extensions in your application.
Add the following code snippet to the Program.cs file to register AI Services and incorporate an OpenAI Assistant in your application:
C#using Azure.AI.OpenAI;
using DevExpress.AIIntegration;
using Microsoft.Extensions.AI;
using System.ClientModel;
//...
string azureOpenAIEndpoint = "AZURE_OPENAI_ENDPOINT";
string azureOpenAIKey = "AZURE_OPENAI_API_KEY";
string deploymentName = "YOUR_MODEL_NAME";
//...
var azureClient = new AzureOpenAIClient(
new Uri(azureOpenAIEndpoint),
new ApiKeyCredential(azureOpenAIKey));
builder.Services.AddChatClient(config =>
config.Use(azureClient.AsChatClient(deploymentName))
);
builder.Services.AddDevExpressAI((config) => {
config.RegisterOpenAIAssistants(azureClient, deploymentName);
});
For additional information on the use of AI Assistants with DxAIChat
and managing messages with custom RAG (Retrieval-Augmented Generation) solutions, refer to the following topic: AI Service Assistants in the DxAIChat component.
[!NOTE]
The availability of Azure Open AI Assistants depends on region. For additional guidance in this regard, refer to the following document: Azure OpenAI Service models – Assistants (Preview).
Files to Review:
Use an AI Assistant with the DevExpress Blazor Grid
This example includes a page with both the DevExpress Blazor Grid (DxGrid
) and Blazor Chat component (DxAIChat
):
To configure our Blazor Grid (data binding and customizations), review the following code file: Grid.razor.
Add AI Chat to the Grid Page
The following code snippet adds the DxAIChat
component to the page:
Razor@using DevExpress.AIIntegration.Blazor.Chat
@using Markdig
<DxGrid @ref="grid" Data="@DataSource" CssClass="my-grid" ShowGroupPanel="true" TextWrapEnabled="false">
@* ... *@
</DxGrid>
<DxAIChat @ref="chat" CssClass="my-grid-chat">
<MessageContentTemplate>
<div class="my-chat-content">
@ToHtml(context.Content)
</div>
</MessageContentTemplate>
</DxAIChat>
@code {
MarkupString ToHtml(string text) {
return (MarkupString)Markdown.ToHtml(text);
}
}
Use the MessageContentTemplate
property to display rich-formatted messages. Use a markdown processor to convert response content to HTML code.
Files to Review:
Set Up the AI Assistant
Handle the OnAfterRenderAsync
event and call the SetupAssistantAsync
method to create your AI assistant and provide it with data and instructions. This example calls our Blazor Grid's ExportToXlsxAsync
method to generate data for the AI Assistant.
Razor@using DevExpress.AIIntegration.OpenAI.Services
@* ... *@
@code {
protected override async Task OnAfterRenderAsync(bool firstRender) {
if(firstRender) {
using(MemoryStream ms = new MemoryStream()) {
grid.BeginUpdate();
grid.ShowGroupedColumns = true;
await grid.ExportToXlsxAsync(ms, new GridXlExportOptions() {
ExportDisplayText = true
});
await chat.SetupAssistantAsync(new OpenAIAssistantOptions("grid_data.xlsx", ms) {
Instructions = AssistantHelper.GetAIAssistantInstructions("xlsx"),
UseFileSearchTool = false
});
grid.ShowGroupedColumns = false;
grid.EndUpdate();
}
}
await base.OnAfterRenderAsync(firstRender);
}
}
You can review and tailor AI assistant instructions in the following file: Instructions.cs.
For information on OpenAI Assistants, refer to the following document: Assistants API overview.
Files to Review:
Add an AI Assistant to the DevExpress Blazor Report Viewer
As you can see in the following image, this sample uses our Blazor Report Viewer alongside the DevExpress Chat component (the AI Assistant tab uses the DxAIChat
component to display requests and responses):
Add New Tab for Your AI Assistant
Use the OnCustomizeTabs
event to add a new tab:
Razor@using DevExpress.AI.Samples.Blazor.Components.Reporting
@using DevExpress.AI.Samples.Blazor.Models
@using DevExpress.Blazor.Reporting.Models
@* ... *@
<DxReportViewer @ref="Viewer" CssClass="my-report" OnCustomizeTabs="OnCustomizeTabs">
</DxReportViewer>
@* ... *@
@code {
// ...
void OnCustomizeTabs(List<TabModel> tabs) {
tabs.Add(new TabModel(new UserAssistantTabContentModel(() => CurrentReport), "AI", "AI Assistant") {
TabTemplate = (tabModel) => {
return (builder) => {
builder.OpenComponent<AITabRenderer>(0);
builder.AddComponentParameter(1, "Model", tabModel.ContentModel);
builder.CloseComponent();
};
}
});
}
}
A new TabModel
object is added to the tab list. The UserAssistantTabContentModel
class implements the ITabContentModel
interface that specifies AI Assistant tab visibility. The tab is only visible when the report is initialized and contains at least one page.
The TabTemplate
property specifies tab content. It dynamically renders the DxAIChat
component inside the tab and passes ContentModel
as a parameter to control tab content.
The content for the AI Assistant tab is defined in the following file: AITabRenderer.razor.
Razor@using DevExpress.AI.Samples.Blazor.Models
@using DevExpress.AIIntegration.Blazor.Chat
@using System.Text.RegularExpressions
@using Markdig
<DxAIChat CssClass="my-report-chat">
<MessageContentTemplate>
<div class="my-chat-content">
@ToHtml(context.Content)
</div>
</MessageContentTemplate>
</DxAIChat>
@code {
[Parameter] public UserAssistantTabContentModel Model { get; set; }
string ClearAnnotations(string text) {
//To clear out annotations in a response from the assistant.
return Regex.Replace(text, @"\【.*?】", "");
}
MarkupString ToHtml(string text) {
text = ClearAnnotations(text);
return (MarkupString)Markdown.ToHtml(text);
}
}
Use the MessageContentTemplate
property to display rich-formatted messages. Use a markdown processor to convert response content to HTML code.
Files to Review:
Set Up the AI Assistant
Handle the Initialized
event and call the SetupAssistantAsync
method to create your AI assistant and provide it with data and instructions. This example calls the ExportToPdf
method to generate data for the AI Assistant:
Razor@using DevExpress.AIIntegration.Blazor.Chat
@using DevExpress.AIIntegration.OpenAI.Services
<DxAIChat CssClass="my-report-chat" Initialized="ChatInitialized">
@* ... *@
</DxAIChat>
@code {
// ...
async Task ChatInitialized(IAIChat aIChat) {
using (MemoryStream ms = Model.GetReportData()) {
await aIChat.SetupAssistantAsync(new OpenAIAssistantOptions("report.pdf", ms) {
Instructions = AssistantHelper.GetAIAssistantInstructions("pdf")
});
}
}
}
You can review and tailor AI assistant instructions in the following file: Instructions.cs.
For information on OpenAI Assistants, refer to the following article: Assistants API overview.
Files to Review:
Files to Review
- Program.cs
- Instructions.cs
- Grid.razor
- ReportViewer.razor
- AITabRenderer.razor
- UserAssistantTabContentModel.cs
Documentation
- Blazor AI Chat
- Demo: Blazor AI Chat
- Blazor Grid
- Blazor Report Viewer
- AI-powered Extensions for DevExpress Reporting
More Examples
- Reporting for Blazor - Integrate AI-powered Summarize and Translate Features based on Azure OpenAI
- Reporting for ASP.NET Core - Integrate AI Assistant based on Azure OpenAI
- AI Chat for Blazor - How to add DxAIChat component in Blazor, MAUI, WPF, and WinForms applications
- Rich Text Editor and HTML Editor for Blazor - How to integrate AI-powered extensions
Does this example address your development requirements/objectives?
(you will be redirected to DevExpress.com to submit your response)
Example Code
C#using System.ClientModel;
using Azure.AI.OpenAI;
using DevExpress.AI.Samples.Blazor.Services;
using DevExpress.AI.Samples.Blazor.Components;
using DevExpress.AI.Samples.Blazor.Data;
using DevExpress.AIIntegration;
using Microsoft.Extensions.AI;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
string azureOpenAIEndpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT");
string azureOpenAIKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
string deploymentName = "gpt4o-big";
var azureClient = new AzureOpenAIClient(
new Uri(azureOpenAIEndpoint),
new ApiKeyCredential(azureOpenAIKey));
builder.Services.AddDevExpressBlazor();
builder.Services.AddChatClient(config =>
config.Use(azureClient.AsChatClient(deploymentName))
);
builder.Services.AddDevExpressServerSideBlazorReportViewer();
builder.Services.AddDevExpressAI((config) => {
config.RegisterOpenAIAssistants(azureClient, deploymentName);
});
builder.Services.AddSingleton<IDemoReportSource, DemoReportSource>();
builder.Services.AddDbContextFactory<IssuesContext>(opt => {
opt.UseSqlite(builder.Configuration.GetConnectionString("IssuesConnectionString"));
});
builder.Services.AddScoped<IssuesDataService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if(!app.Environment.IsDevelopment()) {
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();
Razor@page "/grid"
@using DevExpress.AI.Samples.Blazor.Data;
@using DevExpress.AI.Samples.Blazor.Services
@using DevExpress.AIIntegration.Blazor.Chat
@using DevExpress.AIIntegration.OpenAI.Services
@using Markdig
@inject IssuesDataService IssuesDataService
<DxGrid @ref="grid" Data="@DataSource" CssClass="my-grid" ShowGroupPanel="true" TextWrapEnabled="false" AutoExpandAllGroupRows="true"
CustomizeFilterRowEditor="Grid_CustomizeFilterRowEditor" FilterMenuButtonDisplayMode="GridFilterMenuButtonDisplayMode.Always"
ShowSearchBox="true" ColumnResizeMode="GridColumnResizeMode.NextColumn"
ShowAllRows="true" AllowSelectRowByClick="true" @bind-SearchText="@GridSearchText"
HighlightRowOnHover="true">
<Columns>
<DxGridSelectionColumn Width="75px" />
<DxGridDataColumn FieldName="Name" Caption="Subject" MinWidth="220" AllowGroup="false">
<CellDisplayTemplate>
@GetIssueTypeIconHtml(((Issue)context.DataItem).Type)
@context.HighlightedDisplayText
</CellDisplayTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="ProjectID" Caption="Project" GroupIndex="0" Width="220px">
<EditSettings>
<DxComboBoxSettings Data="ProjectList" ValueFieldName="ID" TextFieldName="Name"
SearchFilterCondition="ListSearchFilterCondition.Contains" />
</EditSettings>
</DxGridDataColumn>
<DxGridDataColumn FieldName="CreatorID" Caption="Owner" Width="140px" MinWidth="100">
<EditSettings>
<DxComboBoxSettings Data="UserList" ValueFieldName="ID" TextFieldName="FullName"
SearchFilterCondition="ListSearchFilterCondition.Contains" />
</EditSettings>
</DxGridDataColumn>
<DxGridDataColumn FieldName="OwnerID" Caption="Assignee" Width="140px" MinWidth="100">
<EditSettings>
<DxComboBoxSettings Data="UserList" ValueFieldName="ID" TextFieldName="FullName"
SearchFilterCondition="ListSearchFilterCondition.Contains" />
</EditSettings>
</DxGridDataColumn>
<DxGridDataColumn FieldName="Status" Caption="Status" Width="140px" MinWidth="140"
TextAlignment="GridTextAlignment.Left">
<EditSettings>
<DxComboBoxSettings Data="StatusList" />
</EditSettings>
<CellDisplayTemplate>
<div class="d-flex align-items-center">
@GetIssueStatusIcon((context.DataItem as Issue).Status)
@context.HighlightedDisplayText
</div>
</CellDisplayTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="CreatedDate" Caption="Created" Width="120px" MinWidth="120" />
<DxGridDataColumn FieldName="ModifiedDate" Caption="Modified" Width="120px" MinWidth="120" />
<DxGridDataColumn FieldName="FixedDate" Caption="Fixed" Width="120px" MinWidth="120" />
<DxGridDataColumn FieldName="Priority" Caption="Priority" Width="90px" TextAlignment="GridTextAlignment.Left"
AllowGroup="false" AllowSort="false">
<FilterRowCellTemplate Context="filterContext">
<DxButton RenderStyle="ButtonRenderStyle.Link" CssClass="p-0 w-100" Enabled="IsGridFiltered()"
Click="@(() => grid.ClearFilter())">Clear</DxButton>
</FilterRowCellTemplate>
<CellDisplayTemplate>
<div>@GetIssuePriorityIconHtml((context.DataItem as Issue).Priority)</div>
</CellDisplayTemplate>
</DxGridDataColumn>
</Columns>
<GroupSummary>
<DxGridSummaryItem FieldName="ID" SummaryType="GridSummaryItemType.Count" />
</GroupSummary>
<TotalSummary>
<DxGridSummaryItem FieldName="ID" SummaryType="GridSummaryItemType.Count" FooterColumnName="Name" />
</TotalSummary>
</DxGrid>
<DxAIChat @ref="chat" CssClass="my-grid-chat">
<MessageContentTemplate>
<div class="my-chat-content">
@ToHtml(context.Content)
</div>
</MessageContentTemplate>
</DxAIChat>
@code {
IGrid grid;
IAIChat chat;
MarkupString ToHtml(string text) {
return (MarkupString)Markdown.ToHtml(text);
}
IEnumerable<Issue> DataSource { get; set; }
IEnumerable<Project> ProjectList { get; set; }
IEnumerable<User> UserList { get; set; }
static List<IssueStatus?> StatusList { get; set; } =
((IssueStatus[])Enum.GetValues(typeof(IssueStatus))).Cast<IssueStatus?>().ToList();
string GridSearchText = "";
[Parameter]
public SizeMode SizeMode { get; set; }
[Parameter]
public EventCallback<Issue> GotoDetailsView { get; set; }
protected override async Task OnInitializedAsync() {
ProjectList = (await IssuesDataService.GetProjectsAsync())
.OrderBy(i => i.Name)
.ToList();
UserList = (await IssuesDataService.GetUsersAsync())
.OrderBy(i => i.FullName)
.ToList();
DataSource = await IssuesDataService.GetIssuesAsync();
}
void Grid_CustomizeFilterRowEditor(GridCustomizeFilterRowEditorEventArgs e) {
if(e.FieldName == "CreatedDate" || e.FieldName == "ModifiedDate" || e.FieldName == "FixedDate")
((ITextEditSettings)e.EditSettings).ClearButtonDisplayMode = DataEditorClearButtonDisplayMode.Never;
}
public MarkupString GetIssueStatusIcon(IssueStatus status) {
string statusIconName = status switch {
IssueStatus.Fixed => "fixed",
IssueStatus.Postponed => "postponed",
IssueStatus.Rejected => "rejected",
IssueStatus.New => "new",
_ => throw new NotSupportedException()
};
string html = string.Format("<span class='status-icon status-icon-{0} me-1 rounded-circle d-inline-block'></span>",
statusIconName);
return new MarkupString(html);
}
public MarkupString GetIssuePriorityIconHtml(IssuePriority priority) {
string priorityClass = "warning";
string title = "Medium";
if(priority == IssuePriority.High) {
priorityClass = "danger";
title = " High ";
}
if(priority == IssuePriority.Low) {
priorityClass = "info";
title = " Low ";
}
string html = string.Format("<span class='badge priority-{0} py-1 px-2' title='{1} Priority'>{1}</span>", priorityClass,
title);
return new MarkupString(html);
}
public MarkupString GetIssueTypeIconHtml(IssueType type) {
string html = "";
if(type == IssueType.Bug)
html = "<span class='bug-icon d-inline-block me-1' title='Bug'></span>";
return new MarkupString(html);
}
public bool IsGridFiltered() {
return !object.ReferenceEquals(grid.GetFilterCriteria(), null);
}
protected override async Task OnAfterRenderAsync(bool firstRender) {
if(firstRender) {
using(MemoryStream ms = new MemoryStream()) {
grid.BeginUpdate();
grid.ShowGroupedColumns = true;
await grid.ExportToXlsxAsync(ms, new GridXlExportOptions() {
ExportDisplayText = true
});
await chat.SetupAssistantAsync(new OpenAIAssistantOptions("grid_data.xlsx", ms) {
Instructions = AssistantHelper.GetAIAssistantInstructions("xlsx"),
UseFileSearchTool = false
});
grid.ShowGroupedColumns = false;
grid.EndUpdate();
}
}
await base.OnAfterRenderAsync(firstRender);
}
}
C#namespace DevExpress.AI.Samples.Blazor {
public static class AssistantHelper {
public static string GetAIAssistantInstructions(string documentFormat) => $"""
You are an analytics assistant specialized in analyzing {documentFormat} files. You use all available methods for parse this data. Your role is to assist users by providing accurate answers to their questions about data contained within these files.
### Tasks:
- Perform various types of data analyses, including summaries, calculations, data filtering, and trend identification.
- Clearly explain your analysis process to ensure users understand how you arrived at your answers.
- Always provide precise and accurate information based on the Excel data.
- If you cannot find an answer based on the provided data, explicitly state: "The requested information cannot be found in the data provided."
### Examples:
1. **Summarization:**
- **User Question:** "What is the average sales revenue for Q1?"
- **Response:** "The average sales revenue for Q1 is calculated as $45,000, based on the data in Sheet1, Column C."
2. **Data Filtering:**
- **User Question:** "Which products had sales over $10,000 in June?"
- **Response:** "The products with sales over $10,000 in June are listed in Sheet2, Column D, and they include Product A, Product B, and Product C."
3. **Insufficient Data:**
- **User Question:** "What is the market trend for Product Z over the past 5 years?"
- **Response:** "The requested information cannot be found in the data provided, as the dataset only includes data for the current year."
### Additional Instructions:
- Format your responses to clearly indicate which sheet and column the data was extracted from when necessary.
- Avoid providing any answers if the data in the file is insufficient for a reliable response.
- Ask clarifying questions if the user's query is ambiguous or lacks detail.
Remember, your primary goal is to provide helpful, data-driven insights that directly answer the user's questions. Do not assume or infer information not present in the dataset.
""";
}
}
Razor@using DevExpress.AI.Samples.Blazor.Models
@using DevExpress.AIIntegration.Blazor.Chat
@using System.Text.RegularExpressions
@using DevExpress.AIIntegration.OpenAI.Services
@using Markdig
<DxAIChat CssClass="my-report-chat" Initialized="ChatInitialized">
<MessageContentTemplate>
<div class="my-chat-content">
@ToHtml(context.Content)
</div>
</MessageContentTemplate>
</DxAIChat>
@code {
[Parameter] public UserAssistantTabContentModel Model { get; set; }
string ClearAnnotations(string text) {
//To clear out annotations in a response from the assistant.
return Regex.Replace(text, @"\【.*?】", "");
}
MarkupString ToHtml(string text) {
text = ClearAnnotations(text);
return (MarkupString)Markdown.ToHtml(text);
}
async Task ChatInitialized(IAIChat aIChat) {
using (MemoryStream ms = Model.GetReportData()) {
await aIChat.SetupAssistantAsync(new OpenAIAssistantOptions("report.pdf", ms) {
Instructions = AssistantHelper.GetAIAssistantInstructions("pdf")
});
}
}
}
Razor@page "/reportviewer"
@using Azure.AI.OpenAI.Assistants
@using DevExpress.Blazor.Reporting
@using DevExpress.XtraReports
@using DevExpress.XtraReports.UI;
@using DevExpress.XtraReports.Parameters;
@using DevExpress.Blazor;
@using DevExpress.AI.Samples.Blazor.Components.Reporting
@using DevExpress.AI.Samples.Blazor.Services;
@using DevExpress.AI.Samples.Blazor.Models;
@using System.IO;
@using DevExpress.Blazor.Reporting.Models;
@using DevExpress.Blazor.Reporting.EditingFields;
<DxListBox Data="@DemoReportList" CssClass="my-list" Value="@DemoReportName" ValueChanged="@(async (string name) => await DemoReportNameChanged(name))"></DxListBox>
<DxReportViewer @ref="Viewer" CssClass="my-report" OnCustomizeTabs="OnCustomizeTabs">
</DxReportViewer>
@code {
[Inject] IDemoReportSource DemoReportSource { get; set; }
List<string> DemoReportList { get; set; }
string DemoReportName { get; set; }
XtraReport CurrentReport { get; set; }
DxReportViewer Viewer { get; set; }
public enum Gender { Male, Female }
async Task DemoReportNameChanged(string name) {
DemoReportName = name;
await UpdateReportAsync(name);
}
async Task UpdateReportAsync(string reportName) {
CurrentReport = GetReport(reportName);
await Viewer.OpenReportAsync(CurrentReport);
}
XtraReport GetReport(string reportName) {
XtraReport report = DemoReportSource.GetReport(reportName);
return report;
}
protected override Task OnInitializedAsync() {
DemoReportList = DemoReportSource.GetReportList().Keys.ToList();
DemoReportName = "Market Share Report";
return base.OnInitializedAsync();
}
protected override async Task OnAfterRenderAsync(bool firstRender) {
if(firstRender) {
await UpdateReportAsync(DemoReportName);
}
base.OnAfterRender(firstRender);
}
void OnCustomizeTabs(List<TabModel> tabs) {
tabs.Add(new TabModel(new UserAssistantTabContentModel(() => CurrentReport), "AI", "AI Assistant") {
TabTemplate = (tabModel) => {
return (builder) => {
builder.OpenComponent<AITabRenderer>(0);
builder.AddComponentParameter(1, "Model", tabModel.ContentModel);
builder.CloseComponent();
};
}
});
}
}
C#using DevExpress.Blazor.Reporting.Models;
using DevExpress.XtraReports.UI;
namespace DevExpress.AI.Samples.Blazor.Models {
public class UserAssistantTabContentModel : ITabContentModel {
public TabContentKind Kind => TabContentKind.Custom;
Func<XtraReport> GetReport;
bool reportReady = false;
public bool GetVisible() => reportReady && (GetReport()?.PrintingSystem?.PageCount ?? 0) > 0;
public UserAssistantTabContentModel(Func<XtraReport> getReport) {
GetReport = getReport;
}
public MemoryStream GetReportData() {
var ms = new MemoryStream();
GetReport()?.PrintingSystem.ExportToPdf(ms);
ms.Position = 0;
return ms;
}
public Task InitializeAsync() {
reportReady = false;
return Task.CompletedTask;
}
public void Update() {
reportReady = true;
}
}
}