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!

Creating XAF Property Editors in a Unified Way for Windows Forms and Blazor Using WebView

Creating XAF Property Editors in a Unified Way for Windows Forms and Blazor Using WebView

Introduction

The eXpressApp Framework (XAF) from DevExpress is a versatile application framework that supports multiple UI platforms, including Windows Forms and Blazor. Maintaining separate property editors for each platform can be cumbersome. This article explores how to create unified property editors for both Windows Forms and Blazor by leveraging WebView for Windows Forms and the Monaco Editor, the editor used in Visual Studio Code.

Blazor Implementation

 

 

Windows forms Implementation

 

Prerequisites

Before we begin, ensure you have the following installed:

  • Visual Studio 2022 or later
  • .NET 8.0 SDK or later
  • DevExpress XAF 22.2 or later

Step 1: Create a XAF Application for Windows Forms and Blazor

  1. Create a New Solution:
    • Open Visual Studio and create a new solution.
    • Add two projects to this solution:
      • A Windows Forms project.
      • A Blazor project.
  2. Set Up XAF:
    • Follow the DevExpress documentation to set up XAF in both projects. Official documentation here

Step 2: Create a Razor Class Library

  1. Create a Razor Class Library:
    • Add a new Razor Class Library project to the solution.
    • Name it XafVsCodeEditor.
  2. Design the Monaco Editor Component:

We are done with the shared library that we will reference in both Blazor and Windows projects.

Step 3: Integrate the Razor Class Library into Windows Forms

  1. Add NuGet References:
    • In the Windows Forms project, add the following NuGet packages:
      • Microsoft.AspNetCore.Components.WebView.WindowsForms
      • XafVsCodeEditor (the Razor Class Library created earlier).
    • You can see all the references in the csproj file.
  2. Change the Project Type: In order to add the ability to host Blazor components, we need to change the project SDK from Microsoft.NET.Sdk to Microsoft.NET.Sdk.Razor.
  3. Add Required Files:
    1. wwwroot: folder to host CSS, JavaScript, and the index.html.
    2. _Imports.razor: this file adds global imports. Source here.
    3. index.html: one of the most important files because it hosts a special blazor.webview.js to interact with the WebView. See here.

Official Microsoft tutorial is available here.

Step 4: Implementing the XAF Property Editors

I’m not going to show the full steps to create the property editors. Instead, I will focus on the most important parts of the editor. Let’s start with Windows.

In Windows Forms, the most important method is when you create the instance of the control, in this case, the WebView. As you can see, this is where you instantiate the services that will be passed as a parameter to the component, in our case, the data model. You can find the full implementation of the property editor for Windows here and the official DevExpress documentation here.

protected override object CreateControlCore()
{
    control = new BlazorWebView();
    control.Dock = DockStyle.Fill;
    var services = new ServiceCollection();
    services.AddWindowsFormsBlazorWebView();
    control.HostPage = "wwwroot\\index.html";
    var tags = MonacoEditorTagHelper.AddScriptTags;
    control.Services = services.BuildServiceProvider();
    parameters = new Dictionary<string, object>();
    if (PropertyValue == null)
    {
        PropertyValue = new MonacoEditorData() { Language = "markdown" };
    }
    parameters.Add("Value", PropertyValue);
    control.RootComponents.Add<MonacoEditorComponent>("#app", parameters);
    control.Size = new System.Drawing.Size(300, 300);
    return control;
}

Now, for the property editor for Blazor, you can find the full source code here and the official DevExpress documentation here.

protected override IComponentModel CreateComponentModel()
{
    var model = new MonacoEditorDataModel();
    model.ValueChanged = EventCallback.Factory.Create<IMonacoEditorData>(this, value => {
        model.Value = value;
        OnControlValueChanged();
        WriteValue();
    });
    return model;
}

One of the most important things to notice here is that in version 24 of XAF, Blazor property editors have been simplified so they require fewer layers of code. The magical databinding happens because in the data model there should be a property of the same value and type as one of the parameters in the Blazor component.

Step 5: Running the Application

Before we run our solution, we need to add a domain object that implements a property of type IMonacoData, which is the interface we associated with our property editor. Here is a sample domain object that has a property of type MonacoEditorData:

[DefaultClassOptions]
public class DomainObject1 : BaseObject, IXafEntityObject
{
    public DomainObject1(Session session) : base(session) { }
    public override void AfterConstruction()
    {
        base.AfterConstruction();
    }

    MonacoEditorData monacoEditorData;
    string text;
    
    public MonacoEditorData MonacoEditorData
    {
        get => monacoEditorData;
        set => SetPropertyValue(nameof(MonacoEditorData), ref monacoEditorData, value);
    }

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

    public void OnCreated()
    {
        this.MonacoEditorData = new MonacoEditorData("markdown", "");
        MonacoEditorData.PropertyChanged += SourceEditor_PropertyChanged;
    }

    void IXafEntityObject.OnSaving()
    {
        this.Text = this.MonacoEditorData.Code;
    }

    void IXafEntityObject.OnLoaded()
    {
        this.MonacoEditorData = new MonacoEditorData("markdown", this.Text);
        MonacoEditorData.PropertyChanged += SourceEditor_PropertyChanged;
    }

    private void SourceEditor_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        this.Text = this.MonacoEditorData.Code;
    }
}

As you can see, DomainObject1 implements the interface IXafEntityObject. We are using the interface events to load and save the content of the editor to the text property.

Now, build and run the solution. You should now have a Windows Forms application that hosts a Blazor property editor using WebView and the Monaco Editor, as well as a Blazor application using the same property editor.

You can find a working example here.

Conclusion

By leveraging WebView and the Monaco Editor, you can create unified property editors for both Windows Forms and Blazor in XAF applications. This approach simplifies maintenance and provides a consistent user experience across different platforms. With the flexibility of Blazor and the robustness of Windows Forms, you can build powerful and versatile property editors that cater to a wide range of user needs.