Example T1268742
Visible to All Users

Blazor Grid and Report Viewer — Incorporate an AI Assistant (Azure OpenAI) in your next DevExpress-powered Blazor app

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:

  1. Register AI Services within the application.
  2. Add the DevExpress Chat component (DxAIChat).
  3. Export component data and pass it to the AI Assistant.

The following DevExpress Blazor Components were used in this sample project:

[!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):

Blazor Grid and Integrated AI Assistant

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):

Blazor Report Viewer and Integrated AI Assistant

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

Documentation

More Examples

Does this example address your development requirements/objectives?

(you will be redirected to DevExpress.com to submit your response)

Example Code

DevExpress.AI.Samples.Blazor/Program.cs
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();
DevExpress.AI.Samples.Blazor/Components/Pages/Grid.razor
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); } }
DevExpress.AI.Samples.Blazor/Instructions.cs
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. """; } }
DevExpress.AI.Samples.Blazor/Components/Reporting/AITabRenderer.razor
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") }); } } }
DevExpress.AI.Samples.Blazor/Components/Pages/ReportViewer.razor
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(); }; } }); } }
DevExpress.AI.Samples.Blazor/Models/UserAssistantTabContentModel.cs
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; } } }

Disclaimer: The information provided on DevExpress.com and affiliated web properties (including the DevExpress Support Center) is provided "as is" without warranty of any kind. Developer Express Inc disclaims all warranties, either express or implied, including the warranties of merchantability and fitness for a particular purpose. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.

Confidential Information: Developer Express Inc does not wish to receive, will not act to procure, nor will it solicit, confidential or proprietary materials and information from you through the DevExpress Support Center or its web properties. Any and all materials or information divulged during chats, email communications, online discussions, Support Center tickets, or made available to Developer Express Inc in any manner will be deemed NOT to be confidential by Developer Express Inc. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.