Understanding XtraReports: A Windows Forms Developer’s Guide

Understanding XtraReports: A Windows Forms Developer’s Guide

Introduction 🎯

If you’re familiar with Windows Forms development, transitioning to XtraReports will feel remarkably natural. This guide explores how XtraReports leverages familiar Windows Forms concepts while extending them for robust reporting capabilities.

💡 Quick Tip: Think of XtraReports as Windows Forms optimized for paper output instead of screen output!

A Personal Journey ✨

Microsoft released .NET Framework in late 2002. At the time, I was a VB6 developer, relying on Crystal Reports 7 for reporting. By 2003, my team was debating whether to transition to this new thing called .NET. We were concerned about VB6’s longevity—thinking it had just a couple more years left. How wrong we were! Even today, VB6 applications are still running in some places (it’s January 2, 2025, as I write this).

Back in the VB6 era, we used the Crystal Reports COM object to integrate reports. When we finally moved to .NET Framework, we performed some “black magic” to continue using our existing 700 reports across nine countries. The decision to fully embrace .NET was repeatedly delayed due to the sheer volume of reports we had to manage. Our ultimate goal was to unify our reporting and parameter forms within a single development environment.

This led us to explore other technologies. While considering Delphi, we discovered DevExpress. My boss procured our first DevExpress .NET license for Windows Forms, marking the start of my adventure with DevExpress and XtraReports. Initially, transitioning from the standalone Crystal Report Designer to the IDE-based XtraReports Designer was challenging. To better understand how XtraReports worked, I decided to write reports programmatically instead of using the visual designer.

Architectural Similarities 🗽️

XtraReports mirrors many fundamental Windows Forms concepts:

Source Destination
XtraReport Class Report Designer Surface
XtraReport Class Control Container
XtraReport Class Event System
XtraReport Class Properties Window
Control Container Labels & Text
Control Container Tables & Grids
Control Container Images & Charts
Report Designer Surface Control Toolbox
Report Designer Surface Design Surface
Report Designer Surface Preview Window

Like how Windows Forms applications start with a Form class, XtraReports begin with an XtraReport base class. Both serve as containers that can:

  • Host other controls
  • Manage layout
  • Handle events
  • Support data binding

Visual Designer Experience 🎨

The design experience remains consistent with Windows Forms:

Windows Forms XtraReports
Form Designer Report Designer
Toolbox Report Controls
Properties Window Properties Grid
Component Tray Component Tool

Control Ecosystem 🧰

XtraReports provides analogous controls to Windows Forms:


// Windows Forms
public partial class CustomerForm : Form
{
    private Label customerNameLabel;
    private DataGridView orderDetailsGrid;
}

// XtraReports
public partial class CustomerReport : XtraReport
{
    private XRLabel customerNameLabel;
    private XRTable orderDetailsTable;
}
    

Common control mappings:

  • Label ➡️ XRLabel
  • Panel ➡️ XRPanel
  • PictureBox ➡️ XRPictureBox
  • DataGridView ➡️ XRTable
  • GroupBox ➡️ Band
  • UserControl ➡️ Subreport

Data Binding Patterns 📊

The data binding syntax maintains familiarity:


// Windows Forms data binding
customerNameLabel.DataBindings.Add("Text", customerDataSet, "Customers.Name");

// XtraReports data binding
customerNameLabel.ExpressionBindings.Add(
    new ExpressionBinding("Text", "[Name]"));
    

Code Architecture 🗍️

The code-behind model remains consistent:


public partial class CustomerReport : DevExpress.XtraReports.UI.XtraReport
{
    public CustomerReport()
    {
        InitializeComponent(); // Familiar Windows Forms pattern
    }

    private void CustomerReport_BeforePrint(object sender, PrintEventArgs e)
    {
        // Event handling similar to Windows Forms
        // Instead of Form_Load, we have Report_BeforePrint
    }
}
    

Key Differences ⚡

While similarities abound, important differences exist:

  1. Output Focus 🖨️
    • Windows Forms: Screen-based interaction
    • XtraReports: Print/export optimization
  2. Layout Model 📜
    • Windows Forms: Flexible screen layouts
    • XtraReports: Page-based layouts with bands
  3. Control Behavior 🎮
    • Windows Forms: Interactive controls
    • XtraReports: Display-oriented controls
  4. Data Processing 🗄️
    • Windows Forms: Real-time data interaction
    • XtraReports: Batch data processing

Some Advices 🌟

  1. Design Philosophy
    
    // Think in terms of paper output
    public class InvoiceReport : XtraReport
    {
        protected override void OnBeforePrint(PrintEventArgs e)
        {
            // Calculate page breaks
            // Optimize for printing
        }
    }
                
  2. Layout Strategy
    • Use bands for logical grouping
    • Consider paper size constraints
    • Plan for different export formats
  3. Data Handling
    • Pre-process data when possible
    • Use calculated fields for complex logic
    • Consider subreports for complex layouts
Guide to Blazor Component Design and Implementation for backend devs

Guide to Blazor Component Design and Implementation for backend devs

Over time, I transitioned to using the first versions of my beloved framework, XAF. As you might know, XAF generates a polished and functional UI out of the box. Using XAF made me more of a backend developer since most of the development work wasn’t visual—especially in the early versions, where the model designer was rudimentary (it’s much better now).

Eventually, I moved on to developing .NET libraries and NuGet packages, diving deep into SOLID design principles. Fun fact: I actually learned about SOLID from DevExpress TV. Yes, there was a time before YouTube when DevExpress posted videos on technical tasks!

Nowadays, I feel confident creating and publishing my own libraries as NuGet packages. However, my “old monster” was still lurking in the shadows: UI components. I finally decided it was time to conquer it, but first, I needed to choose a platform. Here were my options:

  1. Windows Forms: A robust and mature platform but limited to desktop applications.
  2. WPF: A great option with some excellent UI frameworks that I love, but it still feels a bit “Windows Forms-ish” to me.
  3. Xamarin/Maui: I’m a big fan of Xamarin Forms and Xamarin/Maui XAML, but they’re primarily focused on device-specific applications.
  4. Blazor: This was the clear winner because it allows me to create desktop applications using Electron, embed components into Windows Forms, or even integrate with MAUI.

Recently, I’ve been helping my brother with a project in Blazor. (He’s not a programmer, but I am.) This gave me an opportunity to experiment with design patterns to get the most out of my components, which started as plain HTML5 pages.

Without further ado, here are the key insights I’ve gained so far.

Building high-quality Blazor components requires attention to both the C# implementation and Razor markup patterns. This guide combines architectural best practices with practical implementation patterns to create robust, reusable components.

1. Component Architecture and Organization

Parameter Organization

Start by organizing parameters into logical groups for better maintainability:

public class CustomForm : ComponentBase
{
    // Layout Parameters
    [Parameter] public string Width { get; set; }
    [Parameter] public string Margin { get; set; }
    [Parameter] public string Padding { get; set; }
    
    // Validation Parameters
    [Parameter] public bool EnableValidation { get; set; }
    [Parameter] public string ValidationMessage { get; set; }
    
    // Event Callbacks
    [Parameter] public EventCallback<bool> OnValidationComplete { get; set; }
    [Parameter] public EventCallback<string> OnSubmit { get; set; }
}

Corresponding Razor Template

<div class="form-container" style="width: @Width; margin: @Margin; padding: @Padding">
    <form @onsubmit="HandleSubmit">
        @if (EnableValidation)
        {
            <div class="validation-message">
                @ValidationMessage
            </div>
        }
        @ChildContent
    </form>
</div>

2. Smart Default Values and Template Composition

Component Implementation

public class DataTable<T> : ComponentBase
{
    [Parameter] public int PageSize { get; set; } = 10;
    [Parameter] public bool ShowPagination { get; set; } = true;
    [Parameter] public string EmptyMessage { get; set; } = "No data available";
    [Parameter] public IEnumerable<T> Items { get; set; } = Array.Empty<T>();
    [Parameter] public RenderFragment HeaderTemplate { get; set; }
    [Parameter] public RenderFragment<T> RowTemplate { get; set; }
    [Parameter] public RenderFragment FooterTemplate { get; set; }
}

Razor Implementation

<div class="table-container">
    @if (HeaderTemplate != null)
    {
        <header class="table-header">
            @HeaderTemplate
        </header>
    }
    
    <div class="table-content">
        @if (!Items.Any())
        {
            <div class="empty-state">@EmptyMessage</div>
        }
        else
        {
            @foreach (var item in Items)
            {
                @RowTemplate(item)
            }
        }
    </div>
    
    @if (ShowPagination)
    {
        <div class="pagination">
            <!-- Pagination implementation -->
        </div>
    }
</div>

3. Accessibility and Unique IDs

Component Implementation

public class FormField : ComponentBase
{
    private string fieldId = $"field-{Guid.NewGuid():N}";
    private string labelId = $"label-{Guid.NewGuid():N}";
    private string errorId = $"error-{Guid.NewGuid():N}";
    
    [Parameter] public string Label { get; set; }
    [Parameter] public string Error { get; set; }
    [Parameter] public bool Required { get; set; }
}

Razor Implementation

<div class="form-field">
    <label id="@labelId" for="@fieldId">
        @Label
        @if (Required)
        {
            <span class="required" aria-label="required">*</span>
        }
    </label>
    
    <input id="@fieldId" 
           aria-labelledby="@labelId"
           aria-describedby="@errorId"
           aria-required="@Required" />
    
    @if (!string.IsNullOrEmpty(Error))
    {
        <div id="@errorId" class="error-message" role="alert">
            @Error
        </div>
    }
</div>

4. Virtualization and Performance

Component Implementation

public class VirtualizedList<T> : ComponentBase
{
    [Parameter] public IEnumerable<T> Items { get; set; }
    [Parameter] public RenderFragment<T> ItemTemplate { get; set; }
    [Parameter] public int ItemHeight { get; set; } = 50;
    [Parameter] public Func<ItemsProviderRequest, ValueTask<ItemsProviderResult<T>>> ItemsProvider { get; set; }
}

Razor Implementation

<div class="virtualized-container" style="height: 500px; overflow-y: auto;">
    <Virtualize Items="@Items"
                ItemSize="@ItemHeight"
                ItemsProvider="@ItemsProvider"
                Context="item">
        <ItemContent>
            <div class="list-item" style="height: @(ItemHeight)px">
                @ItemTemplate(item)
            </div>
        </ItemContent>
        <Placeholder>
            <div class="loading-placeholder" style="height: @(ItemHeight)px">
                <div class="loading-animation"></div>
            </div>
        </Placeholder>
    </Virtualize>
</div>

Best Practices Summary

1. Parameter Organization

  • Group related parameters with clear comments
  • Provide meaningful default values
  • Use parameter validation where appropriate

2. Template Composition

  • Use RenderFragment for customizable sections
  • Provide default templates when needed
  • Enable granular control over component appearance

3. Accessibility

  • Generate unique IDs for form elements
  • Include proper ARIA attributes
  • Support keyboard navigation

4. Performance

  • Implement virtualization for large datasets
  • Use loading states and placeholders
  • Optimize rendering with appropriate conditions

Conclusion

Building effective Blazor components requires attention to both the C# implementation and Razor markup. By following these patterns and practices, you can create components that are:

  • Highly reusable
  • Performant
  • Accessible
  • Easy to maintain
  • Flexible for different use cases

Remember to adapt these practices to your specific needs while maintaining clean component design principles.

Head Content Injection in .NET 8 Blazor Web Apps

Head Content Injection in .NET 8 Blazor Web Apps

My journey with Microsoft Semantic Kernel marked the beginning of a new adventure: stepping out of my comfort zone as a backend developer to create applications with user interfaces, rather than just building apps for unit and integration testing.

I naturally chose Blazor as my UI framework, and I’ll be sharing my frontend development experiences here. Sometimes it can be frustratingly difficult to accomplish seemingly simple tasks (like centering a div!), but AI assistants like GitHub Copilot have been incredibly helpful in reducing those pain points.

One of my recent challenges involved programmatically including JavaScript and CSS in Blazor applications. I prefer an automated approach rather than manually adding tags to HTML. Back in the .NET 5 era, I wrote an article about using tag helpers for this purpose, which you can find here

However, I recently discovered that my original approach no longer works. I’ve been developing several prototypes using the new DevExpress Chat component, and many of these prototypes include custom components that require JavaScript and CSS. Despite my attempts, I couldn’t get these components to work with the tag helpers, and the reason wasn’t immediately obvious. During the Thanksgiving break, I decided to investigate this issue, and I’d like to share what I found.

With the release of .NET 8, Blazor introduced a new web app template that unifies Blazor Server and WebAssembly into a single project structure. This change affects how we inject content into the document’s head section, particularly when working with Tag Helpers or components.

Understanding the Changes

In previous versions of Blazor, we typically worked with _Host.cshtml for server-side rendering, where traditional ASP.NET Core Tag Helpers could target the <head> element directly. The new .NET 8 Blazor Web App template uses App.razor as the root component and introduces the <HeadOutlet> component for managing head content.

Approach 1: Adapting Tag Helpers

If you’re migrating existing Tag Helpers or creating new ones for head content injection, you’ll need to modify them to target HeadOutlet instead of the head element:


using Microsoft.AspNetCore.Razor.TagHelpers;

namespace YourNamespace
{
    [HtmlTargetElement("HeadOutlet")]
    public class CustomScriptTagHelper : TagHelper
    {
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.PostContent.AppendHtml(
                "<script src=\"_content/YourLibrary/js/script.js\"></script>"
            );
        }
    }
}
    

Remember to register your Tag Helper in _Imports.razor:

@addTagHelper *, YourLibrary

Approach 2: Using Blazor Components (Recommended)

While adapting Tag Helpers works, Blazor offers a more idiomatic approach using components and the HeadContent component. This approach aligns better with Blazor’s component-based architecture:


@namespace YourNamespace
@implements IComponentRenderMode

<HeadContent>
    <script src="_content/YourLibrary/js/script.js"></script>
</HeadContent>
    

To use this component in your App.razor:


<head>
    <!-- Other head elements -->
    <HeadOutlet @rendermode="RenderModeForPage" />
    <YourScriptComponent @rendermode="RenderModeForPage" />
</head>
    

Benefits of the Component Approach

  • Better Integration: Components work seamlessly with Blazor’s rendering model
  • Render Mode Support: Easy to control rendering based on the current render mode (Interactive Server, WebAssembly, or Auto)
  • Dynamic Content: Can leverage Blazor’s full component lifecycle and state management
  • Type Safety: Provides compile-time checking and better tooling support

Best Practices

  • Prefer the component-based approach for new development
  • Use Tag Helpers only when migrating existing code or when you need specific ASP.NET Core pipeline integration
  • Always specify the @rendermode attribute to ensure proper rendering in different scenarios
  • Place custom head content components after HeadOutlet to ensure proper ordering

Conclusion

While both approaches work in .NET 8 Blazor Web Apps, the component-based approach using HeadContent provides a more natural fit with Blazor’s architecture and offers better maintainability and flexibility. When building new applications, consider using components unless you have a specific need for Tag Helper functionality.

Using DevExpress Chat Component and Semantic Kernel ResponseFormat to show a product carousel

Using DevExpress Chat Component and Semantic Kernel ResponseFormat to show a product carousel

Today, when I woke up, it was sunny but really cold, and the weather forecast said that snow was expected.

So, I decided to order ramen and do a “Saturday at home” type of project. My tools of choice for this experiment are:

1) DevExpress Chat Component for Blazor

I’m thrilled they have this component. I once wrote my own chat component, and it’s a challenging task, especially given the variety of use cases.

2) Semantic Kernel

I’ve been experimenting with Semantic Kernel for a while now, and let me tell you—it’s a fantastic tool if you’re in the .NET ecosystem. It’s so cool to have native C# code to interact with AI services in a flexible way, making your code mostly agnostic to the AI provider—like a WCF for AIs.

Goal of the Experiment

The goal for today’s experiment is to render a list of products as a carousel within a chat conversation.

Configuration

To accomplish this, I’ll use prompt execution settings in Semantic Kernel to ensure that the response from the LLM is always in JSON format as a string.

var Settings = new OpenAIPromptExecutionSettings 
{ 
    MaxTokens = 500, 
    Temperature = 0.5, 
    ResponseFormat = "json_object" 
};

The key part here is the response format. The chat completion can respond in two ways:

  • Text: A simple text answer.
  • JSON Object: This format always returns a JSON object, with the structure provided as part of the prompt.

With this approach, we can deserialize the LLM’s response to an object that helps conditionally render the message content within the DevExpress Chat Component.

Structure

Here’s the structure I’m using:

public class MessageData
{
    public string Message { get; set; }
    public List Options { get; set; }
    public string MessageTemplateName { get; set; }
}

public class OptionSet
{
    public string Name { get; set; }
    public string Description { get; set; }
    public List Options { get; set; }
}

public class Option
{
    public string Image { get; set; }
    public string Url { get; set; }
    public string Description { get; set; }
};
  • MessageData: This structure will always be returned by our LLM.
  • Option: A list of options for a message, which also serves as data for possible responses.
  • OptionSet: A list of possible responses to feed into the prompt execution settings.

Prompt Execution Settings

One more step on the Semantic Kernel side is configuring the prompt execution settings:

var Settings = new OpenAIPromptExecutionSettings 
{ 
    MaxTokens = 500, 
    Temperature = 0.5, 
    ResponseFormat = "json_object" 
};

Settings.ChatSystemPrompt = $"You need to answer using this JSON format with this structure {Structure} " +
                            $"Before giving an answer, check if it exists within this list of option sets {OptionSets}. " +
                            $"If your answer does not include options, the message template value should be 'Message'; otherwise, it should be 'Options'.";

In the prompt, we specify the structure {Structure} we want as a response, provide a list of possible options for the message in the {OptionSets} variable, and add a final line to guide the LLM on which template type to use.

Example Requests and Responses

For example, when executing the following request:

  • Prompt: “Show me a list of Halloween costumes for cats.”

We’ll get this response from the LLM:

{
    "Message": "Please select one of the Halloween costumes for cats",
    "Options": [
        {"Image": "./images/catblack.png", "Url": "https://cat.com/black", "Description": "Black cat costume"},
        {"Image": "./images/catwhite.png", "Url": "https://cat.com/white", "Description": "White cat costume"},
        {"Image": "./images/catorange.png", "Url": "https://cat.com/orange", "Description": "Orange cat costume"}
    ],
    "MessageTemplateName": "Options"
}

With this JSON structure, we can conditionally render messages in the chat component as follows:

<DxAIChat CssClass="my-chat" MessageSent="MessageSent">
    <MessageTemplate>
        <div>
            @{
                if (@context.Typing)
                {
                    <span>Loading...</span>
                }
                else
                {
                    MessageData md = null;
                    try
                    {
                        md = JsonSerializer.Deserialize<MessageData>(context.Content);
                    }
                    catch
                    {
                        md = null;
                    }
                    if (md == null)
                    {
                        <div class="my-chat-content">
                            @context.Content
                        </div>
                    }
                    else
                    {
                        if (md.MessageTemplateName == "Options")
                        {
                            <div class="centered-carousel">
                                <Carousel class="carousel-container" Width="280" IsFade="true">
                                    @foreach (var option in md.Options)
                                    {
                                        <CarouselItem>
                                            <ChildContent>
                                                <div>
                                                    <img src="@option.Image" alt="demo-image" />
                                                    <Button Color="Color.Primary" class="carousel-button">@option.Description</Button>
                                                </div>
                                            </ChildContent>
                                        </CarouselItem>
                                    }
                                </Carousel>
                            </div>
                        }
                        else if (md.MessageTemplateName == "Message")
                        {
                            <div class="my-chat-content">
                                @md.Message
                            </div>
                        }
                    }
                }
            }
        </div>
    </MessageTemplate>
</DxAIChat>

End Solution Example

Here’s an example of the final solution:

You can find the full source code here: https://github.com/egarim/devexpress-ai-chat-samples and a short video here https://youtu.be/dxMnOWbe3KA

 

Async Code Execution in XAF Actions

Async Code Execution in XAF Actions

Lately, I’ve been working extensively on interacting with LLMs using the Semantic Kernel framework. My experiments usually start as NUnit test projects, where I prototype my ideas.

Once an experiment is successful, I move it to XAF. Recently, I faced challenges with executing asynchronous code and updating the XAF UI. This process is tricky because some solutions might appear to work but fail under certain conditions.

Goals

Here’s what I aim to achieve:

  1. Execute asynchronous code within the execute handler of an action.
  2. Notify the UI and access the current view, object, and object space.
  3. Run multiple operations on a background thread.

For more background, check out these links on async executions within XAF actions:

WebForms

Blazor

Complete source code for this test can be found on GitHub.

Common Cases

1. Blocking the UI Thread (this will not work)

If you don’t make your action async, attempting to get the awaiter will block the UI thread, freezing your application.


ActionBlockUIThread = new SimpleAction(this, nameof(ActionBlockUIThread), "View");
ActionBlockUIThread.Execute += ActionBlockUIThread_Execute;

protected virtual void ActionBlockUIThread_Execute(object sender, SimpleActionExecuteEventArgs e) {
    var Tasks = GetTaskList();
    StringBuilder Results = new StringBuilder();
    foreach (var item in Tasks) {
        Results.AppendLine(item.Invoke().GetAwaiter().GetResult().ToString());
    }
    MessageOptions options = new MessageOptions { Duration = 2000, Message = Results.ToString(), Type = InformationType.Success };
    Application.ShowViewStrategy.ShowMessage(options);
}

2. Using Async Modifier (somehow works)

Marking your handler as async prevents blocking but keeps the UI responsive, which can allow the user to modify or navigate away from the current view, causing exceptions.


protected virtual async void ActionWithAsyncModifier_Execute(object sender, SimpleActionExecuteEventArgs e) {
    var Tasks = GetTaskList();
    StringBuilder Results = new StringBuilder();
    foreach (var item in Tasks) {
        var CurrentResult = await item.Invoke();
        Results.AppendLine(CurrentResult.ToString());
    }
    MessageOptions options = new MessageOptions { Duration = 2000, Message = Results.ToString(), Type = InformationType.Success };
    Application.ShowViewStrategy.ShowMessage(options);
}

A slightly modified Blazor version of this code also illustrates similar issues.

Executing Object Space Operations Inside Async Action, things that can happen

This approach still leaves the UI responsive, risking disposal of object space if the user navigates away, if that happens you will end up with an exception


protected virtual async void ActionWithAsyncModifierAndOsOperations_Execute(object sender, SimpleActionExecuteEventArgs e) {
    var Instance = GetInstance();
    var Tasks = GetTaskList();
    StringBuilder Results = new StringBuilder();
    foreach (var item in Tasks) {
        var CurrentResult = await item.Invoke();
        Results.AppendLine(CurrentResult.ToString());
    }
    Instance.Result = Results.ToString();
    ViewCommit();
    MessageOptions options = new MessageOptions { Duration = 3000, Message = Instance.Result, Type = InformationType.Success };
    Application.ShowViewStrategy.ShowMessage(options);
}

Proposed Solution

My solution utilizes a background worker to handle async operations while locking the UI thread with a loading indicator. This allows us to react to progress on the UI thread.

Async Background Worker Example

Here’s how the AsyncBackgroundWorker is set up and used:


protected virtual void AsyncActionWithAsyncBackgroundWorker_Execute(object sender, SimpleActionExecuteEventArgs e) {
    var tasks = GetTaskList();
    var worker = new AsyncBackgroundWorker

Handling Background Worker Events


protected virtual void ProcessingDone(Dictionary<int, object> results) {
    // Interact with UI and object space
}

protected virtual void OnReportProgress(int progress, string status, object result) {
    MessageOptions options = new MessageOptions { Duration = 2000, Message = status, Type = InformationType.Success };
    Application.ShowViewStrategy.ShowMessage(options);
}

Blazor Implementation

The Blazor version manages UI locking by showing a loading indicator and reporting progress through the UI thread:

source here: https://github.com/egarim/XafAsyncActions/blob/master/XafAsyncActions.Blazor.Server/Controllers/MyViewControllerBlazor.cs


protected async override void AsyncActionWithAsyncBackgroundWorker_Execute(object sender, SimpleActionExecuteEventArgs e) {
    loading.Hold("Loading");
    base.AsyncActionWithAsyncBackgroundWorker_Execute(sender, e);
}

protected async override void ProcessingDone(Dictionary<int, object> results) {
    base.ProcessingDone(results);
    loading.Release("Loading");
}

When you run this implementation, it will look like this

As you can see the task are being run on the background worker and every time a task is finish is reported back to the U.I thread where we can execute a notification (this is actually optional)

When all the tasks are finished, I hide the loading indicator, and the user can interact with the view again

I hope this article clarifies async execution in XAF. I will update the repository as new scenarios arise.

Querying Semantic Memory with XAF and the DevExpress Chat Component

Querying Semantic Memory with XAF and the DevExpress Chat Component

A few weeks ago, I received the exciting news that DevExpress had released a new chat component (you can read more about it here). This was a big deal for me because I had been experimenting with the Semantic Kernel for almost a year. Most of my experiments fell into three categories:

  1. NUnit projects with no UI (useful when you need to prove a concept).
  2. XAF ASP.NET projects using a large textbox (String with unlimited size in XAF) to emulate a chat control.
  3. XAF applications using a custom chat component that I developed—which, honestly, didn’t look great because I’m more of a backend developer than a UI specialist. Still, the component did the job.

Once I got my hands on the new Chat component, the first thing I did was write a property editor to easily integrate it into XAF. You can read more about property editors in XAF here.

With the Chat component property editor in place, I had the necessary tool to accelerate my experiments with the Semantic Kernel (learn more about the Semantic Kernel here).

The Current Experiment

A few weeks ago, I wrote an implementation of the Semantic Kernel Memory Store using DevExpress’s XPO as the data storage solution. You can read about that implementation here. The next step was to integrate this Semantic Memory Store into XAF, and that’s now done. Details about that process can be found here.

What We Have So Far

  1. A Chat component property editor for XAF.
  2. A Semantic Kernel Memory Store for XPO that’s compatible with XAF.

With these two pieces, we can create an interesting prototype. The goals for this experiment are:

  1. Saving “memories” into a domain object (via XPO).
  2. Querying these memories through the Chat component property editor, using Semantic Kernel chat completions (compatible with all OpenAI APIs).

Step 1: Memory Collection Object

The first thing we need is an object that represents a collection of memories. Here’s the implementation:

[DefaultClassOptions]
public class MemoryChat : BaseObject
{
    public MemoryChat(Session session) : base(session) {}

    public override void AfterConstruction()
    {
        base.AfterConstruction();
        this.MinimumRelevanceScore = 0.20;
    }

    double minimumRelevanceScore;
    string name;

    [Size(SizeAttribute.DefaultStringMappingFieldSize)]
    public string Name
    {
        get => name;
        set => SetPropertyValue(nameof(Name), ref name, value);
    }

    public double MinimumRelevanceScore
    {
        get => minimumRelevanceScore;
        set => SetPropertyValue(nameof(MinimumRelevanceScore), ref minimumRelevanceScore, value);
    }

    [Association("MemoryChat-MemoryEntries")]
    public XPCollection<MemoryEntry> MemoryEntries
    {
        get => GetCollection<MemoryEntry>(nameof(MemoryEntries));
    }
}

This is a simple object. The two main properties are the MinimumRelevanceScore, which is used for similarity searches with embeddings, and the collection of MemoryEntries, where different memories are stored.

Step 2: Adding Memories

The next task is to easily append memories to that collection. I decided to use a non-persistent object displayed in a popup view with a large text area. When the user confirms the action in the dialog, the text gets vectorized and stored as a memory in the collection. You can see the implementation of the view controller here.

Let me highlight the important parts.

When we create the view for the popup window:

private void AppendMemory_CustomizePopupWindowParams(object sender, CustomizePopupWindowParamsEventArgs e)
{
    var os = this.Application.CreateObjectSpace(typeof(TextMemory));
    var textMemory = os.CreateObject<TextMemory>();
    e.View = this.Application.CreateDetailView(os, textMemory);
}

The goal is to show a large textbox where the user can type any text. When they confirm, the text is vectorized and stored as a memory.

Next, storing the memory:

private async void AppendMemory_Execute(object sender, PopupWindowShowActionExecuteEventArgs e)
{
    var textMemory = e.PopupWindowViewSelectedObjects[0] as TextMemory;
    var currentMemoryChat = e.SelectedObjects[0] as MemoryChat;

    var store = XpoMemoryStore.ConnectAsync(xafEntryManager).GetAwaiter().GetResult();
    var semanticTextMemory = GetSemanticTextMemory(store);
    await semanticTextMemory.SaveInformationAsync(currentMemoryChat.Name, id: Guid.NewGuid().ToString(), text: textMemory.Content);
}

Here, the GetSemanticTextMemory method plays a key role:

private static SemanticTextMemory GetSemanticTextMemory(XpoMemoryStore store)
{
    var embeddingModelId = "text-embedding-3-small";
    var getKey = () => Environment.GetEnvironmentVariable("OpenAiTestKey", EnvironmentVariableTarget.Machine);

    var kernel = Kernel.CreateBuilder()
        .AddOpenAIChatCompletion(ChatModelId, getKey.Invoke())
        .AddOpenAITextEmbeddingGeneration(embeddingModelId, getKey.Invoke())
        .Build();

    var embeddingGenerator = new OpenAITextEmbeddingGenerationService(embeddingModelId, getKey.Invoke());
    return new SemanticTextMemory(store, embeddingGenerator);
}

This method sets up an embedding generator used to create semantic memories.

Step 3: Querying Memories

To query the stored memories, I created a non-persistent type that interacts with the chat component:

public interface IMemoryData
{
    IChatCompletionService ChatCompletionService { get; set; }
    SemanticTextMemory SemanticTextMemory { get; set; }
    string CollectionName { get; set; }
    string Prompt { get; set; }
    double MinimumRelevanceScore { get; set; }
}

This interface provides the necessary services to interact with the chat component, including ChatCompletionService and SemanticTextMemory.

Step 4: Handling Messages

Lastly, we handle message-sent callbacks, as explained in this article:

async Task MessageSent(MessageSentEventArgs args)
{
    ChatHistory.AddUserMessage(args.Content);

    var answers = Value.SemanticTextMemory.SearchAsync(
        collection: Value.CollectionName,
        query: args.Content,
        limit: 1,
        minRelevanceScore: Value.MinimumRelevanceScore,
        withEmbeddings: true
    );

    string answerValue = "No answer";
    await foreach (var answer in answers)
    {
        answerValue = answer.Metadata.Text;
    }

    string messageContent = answerValue == "No answer"
        ? "There are no memories containing the requested information."
        : await Value.ChatCompletionService.GetChatMessageContentAsync($"You are an assistant queried for information. Use this data: {answerValue} to answer the question: {args.Content}.");

    ChatHistory.AddAssistantMessage(messageContent);
    args.SendMessage(new Message(MessageRole.Assistant, messageContent));
}

Here, we intercept the message, query the SemanticTextMemory, and use the results to generate an answer with the chat completion service.

This was a long post, but I hope it’s useful for you all. Until next time—XAF OUT!

You can find the full implementation on this repo 

Rewriting the XPO Semantic Kernel Memory Store to be Compatible with XAF

Rewriting the XPO Semantic Kernel Memory Store to be Compatible with XAF

A few weeks ago, I forked the Semantic Kernel repository to experiment with it. One of my first experiments was to create a memory provider for XPO. The task was not too difficult; basically, I needed to implement the IMemoryStore interface, add some XPO boilerplate code, and just like that, we extended the Semantic Kernel memory store to support 10+ databases. You can check out the code for the XpoMemoryStore here.

My initial goal in creating the XpoMemoryStore was simply to see if XPO would be a good fit for handling embeddings. Spoiler alert: it was! To understand the basic functionality of the plugin, you can take a look at the integration test here.

As you can see, usage is straightforward. You start by connecting to the database that handles embedding collections, and all you need is a valid XPO connection string:

using XpoMemoryStore db = await XpoMemoryStore.ConnectAsync("XPO connection string");

In my original design, everything worked fine, but I faced some challenges when trying to use my new XpoMemoryStore in XAF. Here’s what I encountered:

  1. The implementation of XpoMemoryStore uses its own data layer, which can lead to issues. This needs to be rewritten to use the same data layer as XAF.
  2. The XpoEntry implementation cannot be extended. In some use cases, you might want to use a different object to store the embeddings, perhaps one that has an association with another object.

To address these problems, I introduced the IXpoEntryManager interface. The goal of this interface is to handle object creation and queries.


public interface IXpoEntryManager
{
    T CreateObject();
    public event EventHandler ObjectCreatedEvent;
    void Commit();
    IQueryable GetQuery(bool inTransaction = true);
    void Delete(object instance);
    void Dispose();
}
    

Now, object creation is handled through the CreateObject<T> method, allowing the underlying implementation to be changed to use a UnitOfWork or ObjectSpace. There’s also the ObjectCreatedEvent event, which lets you access the newly created object in case you need to associate it with another object. Lastly, the GetQuery<T> method enables redirecting the search for records to a different type.

I’ll keep updating the code as needed. If you’d like to discuss AI, XAF, or .NET, feel free to schedule a meeting: Schedule a Meeting with us.

Until next time, XAF out!

Related Article

https://www.jocheojeda.com/2024/09/04/using-the-imemorystore-interface-and-devexpress-xpo-orm-to-implement-a-custom-memory-store-for-semantic-kernel/

AI-Powered XtraReports in XAF: Unlocking DevExpress Enhancements

AI-Powered XtraReports in XAF: Unlocking DevExpress Enhancements

Today is Friday, so I decided to take it easy with my integration research. When I woke up, I decided that I just wanted to read the source code of DevExpress AI integrations to get inspired. I began by reading the official blog post about AI and reporting (DevExpress Blog Post). Then, as usual, I proceeded to fork the repository to make my own modifications.

After completing the typical cloning procedure in Visual Studio, I realized that to use the AI functionalities of XtraReport, you don’t need any special version of the report viewer.

The only requirement is to have the NuGet reference as shown below:


    <ItemGroup>
        <PackageReference Include="DevExpress.AIIntegration.Blazor.Reporting.Viewer" Version="24.2.1-alpha-24260" />
    </ItemGroup>
    

Then, add the report integration as shown below:


    config.AddBlazorReportingAIIntegration(config =>
    {
        config.SummarizeBehavior = SummarizeBehavior.Abstractive;
        config.AvailableLanguages = new List<LanguageItem>
        {
            new LanguageItem { Key = "de", Text = "German" },
            new LanguageItem { Key = "es", Text = "Spanish" },
            new LanguageItem { Key = "en", Text = "English" },
            new LanguageItem { Key = "ru", Text = "Russian" },
            new LanguageItem { Key = "it", Text = "Italian" }
        };
    });
    

After completing these steps, your report viewer will display a little star in the options menu, where you can invoke the AI operations.

You can find the source code for this example in my GitHub repository: https://github.com/egarim/XafSmartEditors

Till next time, XAF out!!!

The New Era of Smart Editors: Creating a RAG system using XAF and the new Blazor chat component

The New Era of Smart Editors: Creating a RAG system using XAF and the new Blazor chat component

The New Era of Smart Editors: Developer Express and AI Integration

The new era of smart editors is already here. Developer Express has introduced AI functionality in many of their controls for .NET (Windows Forms, Blazor, WPF, MAUI).

This advancement will eventually come to XAF, but in the meantime, here at XARI, we are experimenting with XAF integrations to add value to our customers.

In this article, we are going to integrate the new chat component into an XAF application, and our first use case will be RAG (Retrieval-Augmented Generation). RAG is a system that combines external data sources with AI-generated responses, improving accuracy and relevance in answers by retrieving information from a document set or knowledge base and using it in conjunction with AI predictions.

To achieve this integration, we will follow the steps outlined in this tutorial:

Implement a Property Editor Based on Custom Components (Blazor)

Implementing the Property Editor

When I implement my own property editor, I usually avoid doing so for primitive types because, in most cases, my property editor will need more information than a simple primitive value. For this implementation, I want to handle a custom value in my property editor. I typically create an interface to represent the type, ensuring compatibility with both XPO and EF Core.

namespace XafSmartEditors.Razor.RagChat
{
    public interface IRagData
    {
        Stream FileContent { get; set; }
        string Prompt { get; set; }
        string FileName { get; set; }
    }
}

Non-Persistent Implementation

After defining the type for my editor, I need to create a non-persistent implementation:

namespace XafSmartEditors.Razor.RagChat
{
    [DomainComponent]
    public class IRagDataImp : IRagData, IXafEntityObject, INotifyPropertyChanged
    {
        private void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        public IRagDataImp()
        {
            Oid = Guid.NewGuid();
        }

        [DevExpress.ExpressApp.Data.Key]
        [Browsable(false)]  
        public Guid Oid { get; set; }

        private string prompt;
        private string fileName;
        private Stream fileContent;

        public Stream FileContent
        {
            get => fileContent;
            set
            {
                if (fileContent == value) return;
                fileContent = value;
                OnPropertyChanged();
            }
        }

        public string FileName
        {
            get => fileName;
            set
            {
                if (fileName == value) return;
                fileName = value;
                OnPropertyChanged();
            }
        }
        
        public string Prompt
        {
            get => prompt;
            set
            {
                if (prompt == value) return;
                prompt = value;
                OnPropertyChanged();
            }
        }

        // IXafEntityObject members
        void IXafEntityObject.OnCreated() { }
        void IXafEntityObject.OnLoaded() { }
        void IXafEntityObject.OnSaving() { }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

Creating the Blazor Chat Component

Now, it’s time to create our Blazor component and add the new DevExpress chat component for Blazor:

<DxAIChat CssClass="my-chat" Initialized="Initialized" 
          RenderMode="AnswerRenderMode.Markdown" 
          UseStreaming="true"
          SizeMode="SizeMode.Medium">
    <EmptyMessageAreaTemplate>
        <div class="my-chat-ui-description">
            <span style="font-weight: bold; color: #008000;">Rag Chat</span> Assistant is ready to answer your questions.
        </div>
    </EmptyMessageAreaTemplate>
    <MessageContentTemplate>
        <div class="my-chat-content">
            @ToHtml(context.Content)
        </div>
    </MessageContentTemplate>
</DxAIChat>

@code {
    IRagData _value;
    [Parameter]
    public IRagData Value
    {
        get => _value;
        set => _value = value;
    }
    
    async Task Initialized(IAIChat chat)
    {
        await chat.UseAssistantAsync(new OpenAIAssistantOptions(
            this.Value.FileName,
            this.Value.FileContent,
            this.Value.Prompt
        ));
    }

    MarkupString ToHtml(string text)
    {
        return (MarkupString)Markdown.ToHtml(text);
    }
}

The main takeaway from this component is that it receives a parameter named Value of type IRagData, and we use this value to initialize the IAIChat service in the Initialized method.

Creating the Component Model

With the interface and domain component in place, we can now create the component model to communicate the value of our domain object with the Blazor component:

namespace XafSmartEditors.Razor.RagChat
{
    public class RagDataComponentModel : ComponentModelBase
    {
        public IRagData Value
        {
            get => GetPropertyValue<IRagData>();
            set => SetPropertyValue(value);
        }

        public EventCallback<IRagData> ValueChanged
        {
            get => GetPropertyValue<EventCallback<IRagData>>();
            set => SetPropertyValue(value);
        }

        public override Type ComponentType => typeof(RagChat);
    }
}

Creating the Property Editor

Finally, let’s create the property editor class that serves as a bridge between XAF and the new component:

namespace XafSmartEditors.Blazor.Server.Editors
{
    [PropertyEditor(typeof(IRagData), true)]
    public class IRagDataPropertyEditor : BlazorPropertyEditorBase, IComplexViewItem
    {
        private IObjectSpace _objectSpace;
        private XafApplication _application;

        public IRagDataPropertyEditor(Type objectType, IModelMemberViewItem model) : base(objectType, model) { }

        public void Setup(IObjectSpace objectSpace, XafApplication application)
        {
            _objectSpace = objectSpace;
            _application = application;
        }

        public override RagDataComponentModel ComponentModel => (RagDataComponentModel)base.ComponentModel;

        protected override IComponentModel CreateComponentModel()
        {
            var model = new RagDataComponentModel();

            model.ValueChanged = EventCallback.Factory.Create<IRagData>(this, value =>
            {
                model.Value = value;
                OnControlValueChanged();
                WriteValue();
            });

            return model;
        }

        protected override void ReadValueCore()
        {
            base.ReadValueCore();
            ComponentModel.Value = (IRagData)PropertyValue;
        }

        protected override object GetControlValueCore() => ComponentModel.Value;

        protected override void ApplyReadOnly()
        {
            base.ApplyReadOnly();
            ComponentModel?.SetAttribute("readonly", !AllowEdit);
        }
    }
}

Bringing It All Together

Now, let’s create a domain object that can feed the content of a file to our chat component:

namespace XafSmartEditors.Module.BusinessObjects
{
    [DefaultClassOptions]
    public class PdfFile : BaseObject
    {
        public PdfFile(Session session) : base(session) { }

        string prompt;
        string name;
        FileData file;

        public FileData File
        {
            get => file;
            set => SetPropertyValue(nameof(File), ref file, value);
        }

        public string Name
        {
            get => name;
            set => SetPropertyValue(nameof(Name), ref name, value);
        }

        public string Prompt
        {
            get => prompt;
            set => SetPropertyValue(nameof(Prompt), ref prompt, value);
        }
    }
}

Creating the Controller

We are almost done! Now, we need to create a controller with a popup action:

namespace XafSmartEditors.Module.Controllers
{
    public class OpenChatController : ViewController
    {
        Popup

WindowShowAction Chat;

        public OpenChatController()
        {
            this.TargetObjectType = typeof(PdfFile);
            Chat = new PopupWindowShowAction(this, "ChatAction", "View");
            Chat.Caption = "Chat";
            Chat.ImageName = "artificial_intelligence";
            Chat.Execute += Chat_Execute;
            Chat.CustomizePopupWindowParams += Chat_CustomizePopupWindowParams;
        }

        private void Chat_Execute(object sender, PopupWindowShowActionExecuteEventArgs e) { }

        private void Chat_CustomizePopupWindowParams(object sender, CustomizePopupWindowParamsEventArgs e)
        {
            PdfFile pdfFile = this.View.CurrentObject as PdfFile;
            var os = this.Application.CreateObjectSpace(typeof(ChatView));
            var chatView = os.CreateObject<ChatView>();

            MemoryStream memoryStream = new MemoryStream();
            pdfFile.File.SaveToStream(memoryStream);
            memoryStream.Seek(0, SeekOrigin.Begin);

            chatView.RagData = os.CreateObject<IRagDataImp>();
            chatView.RagData.FileName = pdfFile.File.FileName;
            chatView.RagData.Prompt = !string.IsNullOrEmpty(pdfFile.Prompt) ? pdfFile.Prompt : DefaultPrompt;
            chatView.RagData.FileContent = memoryStream;

            DetailView detailView = this.Application.CreateDetailView(os, chatView);
            detailView.Caption = $"Chat with Document | {pdfFile.File.FileName.Trim()}";

            e.View = detailView;
        }
    }
}

Conclusion

That’s everything we need to create a RAG system using XAF and the new DevExpress Chat component. You can find the complete source code here: GitHub Repository.

If you want to meet and discuss AI, XAF, and .NET, feel free to schedule a meeting: Schedule a Meeting.

Until next time, XAF out!

Integrating DevExpress Chat Component with Semantic Kernel: A Step-by-Step Guide

Integrating DevExpress Chat Component with Semantic Kernel: A Step-by-Step Guide

Are you excited to bring powerful AI chat completions to your web application? I sure am! In this post, we’ll walk through how to integrate the DevExpress Chat component with the Semantic Kernel using OpenAI. This combination can make your app more interactive and intelligent, and it’s surprisingly simple to set up. Let’s dive in!

Step 1: Adding NuGet Packages

First, let’s ensure we have all the necessary packages. Open your DevExpress.AI.Samples.Blazor.csproj file and add the following NuGet references:

 <ItemGroup>
<PackageReference Include="Microsoft.KernelMemory.Abstractions" Version="0.78.241007.1" />
<PackageReference Include="Microsoft.KernelMemory.Core" Version="0.78.241007.1" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.21.1" />
</ItemGroup>

 

This will bring in the core components of Semantic Kernel to power your chat completions.

Step 2: Setting Up Your Kernel in Program.cs

Next, we’ll configure the Semantic Kernel and OpenAI integration. Add the following code in your Program.cs to create the kernel and set up the chat completion service:


    //Create your OpenAI client
    string OpenAiKey = Environment.GetEnvironmentVariable("OpenAiTestKey");
    var client = new OpenAIClient(new System.ClientModel.ApiKeyCredential(OpenAiKey));

    //Adding semantic kernel
    var KernelBuilder = Kernel.CreateBuilder();
    KernelBuilder.AddOpenAIChatCompletion("gpt-4o", client);
    var sk = KernelBuilder.Build();
    var ChatService = sk.GetRequiredService<IChatCompletionService>();
    builder.Services.AddSingleton<IChatCompletionService>(ChatService);
    

This step is crucial because it connects your app to OpenAI via the Semantic Kernel and sets up the chat completion service that will drive the AI responses in your chat.

Step 3: Creating the Chat Component

Now that we’ve got our services ready, it’s time to set up the chat component. We’ll define the chat interface in our Razor page. Here’s how you can do that:

Razor Section:


    @page "/sk"
    @using DevExpress.AIIntegration.Blazor.Chat
    @using AIIntegration.Services.Chat;
    @using Microsoft.SemanticKernel.ChatCompletion
    @using System.Diagnostics
    @using System.Text.Json
    @using System.Text

    

    @inject IChatCompletionService chatCompletionsService;
    @inject IJSRuntime JSRuntime;
    

This UI will render a clean chat interface using DevExpress’s DxAIChat component, which is connected to our Semantic Kernel chat completion service.

Code Section:

Now, let’s handle the interaction logic. Here’s the code that powers the chat backend:


    @code {

        ChatHistory ChatHistory = new ChatHistory();

        async Task MessageSent(MessageSentEventArgs args)
        {
            // Add the user's message to the chat history
            ChatHistory.AddUserMessage(args.Content);

            // Get a response from the chat completion service
            var Result = await chatCompletionsService.GetChatMessageContentAsync(ChatHistory);

            // Extract the response content
            string MessageContent = Result.InnerContent.ToString();
            Debug.WriteLine("Message from chat completion service:" + MessageContent);

            // Add the assistant's message to the history
            ChatHistory.AddAssistantMessage(MessageContent);

            // Send the response to the UI
            var message = new Message(MessageRole.Assistant, MessageContent);
            args.SendMessage(message);
        }
    }
    

With this in place, every time the user sends a message, the chat completion service will process the conversation history and generate a response from OpenAI. The result is then displayed in the chat window.

Step 4: Run Your Project

Before running the project, ensure that the correct environment variable for the OpenAI key is set (OpenAiTestKey). This key is necessary for the integration to communicate with OpenAI’s API.

Now, you’re ready to test! Simply run your project and navigate to https://localhost:58108/sk. Voilà! You’ll see a beautiful, AI-powered chat interface waiting for your input. 🎉

Conclusion

And that’s it! You’ve successfully integrated the DevExpress Chat component with the Semantic Kernel for AI-powered chat completions. Now, you can take your user interaction to the next level with intelligent, context-aware responses. The possibilities are endless with this integration—whether you’re building a customer support chatbot, a productivity assistant, or something entirely new.

Let me know how your integration goes, and feel free to share what cool things you build with this!

here is the full implementation GitHub