by Joche Ojeda | Jan 8, 2026 | C#, XAF
Async/await in C# is often described as “non-blocking,” but that description hides an important detail:
await is not just about waiting — it is about where execution continues afterward.
Understanding that single idea explains:
- why deadlocks happen,
- why
ConfigureAwait(false) exists,
- and why it *reduces* damage without fixing the root cause.
This article is not just theory. It’s written because this exact class of problem showed up again in real production code during the first week of 2026 — and it took a context-level fix to resolve it.
The Hidden Mechanism: Context Capture
When you await a task, C# does two things:
- It pauses the current method until the awaited task completes.
- It captures the current execution context (if one exists) so the continuation can resume there.
That context might be:
- a UI thread (WPF, WinForms, MAUI),
- a request context (classic ASP.NET),
- or no special context at all (ASP.NET Core, console apps).
This default behavior is intentional. It allows code like this to work safely:
var data = await LoadAsync();
MyLabel.Text = data.Name; // UI-safe continuation
But that same mechanism becomes dangerous when async code is blocked synchronously.
The Root Problem: Blocking on Async
Deadlocks typically appear when async code is forced into a synchronous shape:
var result = GetDataAsync().Result; // or .Wait()
What happens next:
- The calling thread blocks, waiting for the async method to finish.
- The async method completes its awaited operation.
- The continuation tries to resume on the original context.
- That context is blocked.
- Nothing can proceed.
💥 Deadlock.
This is not an async bug. This is a context dependency cycle.
The Blast Radius Concept
Blocking on async is the explosion.
The blast radius is how much of the system is taken down with it.
Full blast (default await)
- Continuation *requires* the blocked context
- The async operation cannot complete
- The caller never unblocks
- Everything stops
Reduced blast (ConfigureAwait(false))
- Continuation does not require the original context
- It resumes on a thread pool thread
- The async operation completes
- The blocking call unblocks
The original mistake still exists — but the damage is contained.
The real fix is “don’t block on async,”
but ConfigureAwait(false) reduces the blast radius when someone does.
What ConfigureAwait(false) Actually Does
await SomeAsyncOperation().ConfigureAwait(false);
This tells the runtime:
“I don’t need to resume on the captured context. Continue wherever it’s safe to do so.”
Important clarifications:
- It does not make code faster by default
- It does not make code parallel
- It does not remove the need for proper async flow
- It only removes context dependency
Why This Matters in Real Code
Async code rarely exists in isolation.
A method often awaits another method, which awaits another:
await AAsync();
await BAsync();
await CAsync();
If any method in that chain requires a specific context, the entire chain becomes context-bound.
That is why:
- library code must be careful,
- deep infrastructure layers must avoid context assumptions,
- and UI layers must be explicit about where context is required.
When ConfigureAwait(false) Is the Right Tool
Use it when all of the following are true:
- The method does not interact with UI state
- The method does not depend on a request context
- The method is infrastructure, library, or backend logic
- The continuation does not care which thread resumes it
This is especially true for:
- NuGet packages
- shared libraries
- data access layers
- network and IO pipelines
What It Is Not
ConfigureAwait(false) is not:
- a fix for bad async usage
- a substitute for proper async flow
- a reason to block on tasks
- something to blindly apply everywhere
It is a damage-control tool, not a cure.
A Real Incident: When None of the Usual Fixes Worked
First week of 2026.
The first task I had with the programmers in my office was to investigate a problem in a trading block. The symptoms looked like a classic async issue: timing bugs, inconsistent behavior, and freezes that felt “await-shaped.”
We did what experienced .NET teams typically do when async gets weird:
- Reviewed the full async/await chain end-to-end
- Double-checked the source code carefully (everything looked fine)
- Tried the usual “tools people reach for” under pressure:
.Wait()
.GetAwaiter().GetResult()
- wrapping in
Task.Run(...)
- adding
ConfigureAwait(false)
- mixing combinations of those approaches
None of it reliably fixed the problem.
At that point it stopped being a “missing await” story. It became a “the model is right but reality disagrees” story.
One of the programmers, Daniel, and I went deeper. I found myself mentally replaying every async pattern I know — especially because I’ve written async-heavy code myself, including library work like SyncFramework, where I synchronize databases and deal with long-running operations.
That’s the moment where this mental model matters: it forces you to stop treating await like syntax and start treating it like mechanics.
The Actual Root Cause: It Was the Context
In the end, the culprit wasn’t which pattern we used — it was where the continuation was allowed to run.
This application was built on DevExpress XAF. In this environment, the “correct” continuation behavior is often tied to XAF’s own scheduling and application lifecycle rules. XAF provides a mechanism to run code in its synchronization context — for example using BlazorApplication.InvokeAsync, which ensures that continuations run where the framework expects.
Once we executed the problematic pipeline through XAF’s synchronization context, the issue was solved.
No clever pattern. No magical await. No extra parallelism.
Just: the right context.
And this is not unique to XAF. Similar ideas exist in:
- Windows Forms (UI thread affinity + SynchronizationContext)
- WPF (Dispatcher context)
- Any framework that requires work to resume on a specific thread/context
Why I’m Writing This
What I wanted from this experience is simple: don’t forget it.
Because what makes this kind of incident dangerous is that it looks like a normal async bug — and the internet is full of “four fixes” people cycle through:
- add/restore missing
await
- use
.Wait() / .Result
- wrap in
Task.Run()
- use
ConfigureAwait(false)
Sometimes those are relevant. Sometimes they’re harmful. And sometimes… they’re all beside the point.
In our case, the missing piece was framework context — and once you see that, you realize why the “blast radius” framing is so useful:
- Blocking is the explosion.
ConfigureAwait(false) contains damage when someone blocks.
- If a framework requires a specific synchronization context, the fix may be to supply the correct context explicitly.
That’s what happened here. And that’s why I’m capturing it as live knowledge, not just documentation.
The Mental Model to Keep
- Async bugs are often context bugs
- Blocking creates the explosion
- Context capture determines the blast radius
ConfigureAwait(false) limits the damage
- Proper async flow prevents the explosion entirely
- Frameworks may require their own synchronization context
- Correct async code can still fail in the wrong context
Async is not just about tasks. It’s about where your code is allowed to continue.
by Joche Ojeda | Jan 12, 2025 | ADO.NET, C#, CPU, dotnet, ORM, XAF, XPO
Introduction
In the .NET ecosystem, “AnyCPU” is often considered a silver bullet for cross-platform deployment. However, this assumption can lead to significant problems when your application depends on native assemblies. In this post, I want to share a personal story that highlights how I discovered these limitations and how native dependencies affect the true portability of AnyCPU applications, especially for database access through ADO.NET and popular ORMs.
My Journey to Understanding AnyCPU’s Limitations
Every year, around Thanksgiving or Christmas, I visit my friend, brother, and business partner Javier. Two years ago, during one of these visits, I made a decision that would lead me to a pivotal realization about AnyCPU architecture.
At the time, I was tired of traveling with my bulky MSI GE72 Apache Pro-24 gaming laptop. According to MSI’s official specifications, it weighed 5.95 pounds—but that number didn’t include the hefty charger, which brought the total to around 12 pounds. Later, I upgraded to an MSI GF63 Thin, which was lighter at 4.10 pounds—but with the charger, it was still around 7.5 pounds. Lugging these laptops through airports felt like a workout.
Determined to travel lighter, I purchased a MacBook Air with the M2 chip. At just 2.7 pounds, including the charger, the MacBook Air felt like a breath of fresh air. The Apple Silicon chip was incredibly fast, and I immediately fell in love with the machine.
Having used a MacBook Pro with Bootcamp and Windows 7 years ago, I thought I could recreate that experience by running a Windows virtual machine on my MacBook Air to check projects and do some light development while traveling.
The Virtualization Experiment
As someone who loves virtualization, I eagerly set up a Windows virtual machine on my MacBook Air. I grabbed my trusty Windows x64 ISO, set up the virtual machine, and attempted to boot it—but it failed. I quickly realized the issue was related to CPU architecture. My x64 ISO wasn’t compatible with the ARM-based M2 chip.
Undeterred, I downloaded a Windows 11 ISO for ARM architecture and created the VM. Success! Windows was up and running, and I installed Visual Studio along with my essential development tools, including DevExpress XPO (my favorite ORM).
The Demo Disaster
The real test came during a trip to Dubai, where I was scheduled to give a live demo showcasing how quickly you can develop Line-of-Business (LOB) apps with XAF. Everything started smoothly until I tried to connect my XAF app to the database. Despite my best efforts, the connection failed.
In the middle of the demo, I switched to an in-memory data provider to salvage the presentation. After the demo, I dug into the issue and realized the root cause was related to the CPU architecture. The native database drivers I was using weren’t compatible with the ARM architecture.
A Familiar Problem
This situation reminded me of the transition from x86 to x64 years ago. Back then, I encountered similar issues where native drivers wouldn’t load unless they matched the process architecture.
The Native Dependency Challenge
Platform-Specific Loading Requirements
Native DLLs must exactly match the CPU architecture of your application:
- If your app runs as x86, it can only load x86 native DLLs.
- If running as x64, it requires x64 native DLLs.
- ARM requires ARM-specific binaries.
- ARM64 requires ARM64-specific binaries.
There is no flexibility—attempting to load a DLL compiled for a different architecture results in an immediate failure.
How Native Libraries are Loaded
When your application loads a native DLL, the operating system follows a specific search pattern:
- The application’s directory
- System directories (System32/SysWOW64)
- Directories listed in the PATH environment variable
Crucially, these native libraries must match the exact architecture of the running process.
// This seemingly simple code
[DllImport("native.dll")]
static extern void NativeMethod();
// Actually requires:
// - native.dll compiled for x86 when running as 32-bit
// - native.dll compiled for x64 when running as 64-bit
// - native.dll compiled for ARM64 when running on ARM64
The SQL Server Example
Let’s look at SQL Server connectivity, a common scenario where the AnyCPU illusion breaks down:
// Traditional ADO.NET connection
using (var connection = new SqlConnection(connectionString))
{
// This requires SQL Native Client
// Which must match the process architecture
await connection.OpenAsync();
}
Even though your application is compiled as AnyCPU, the SQL Native Client must match the process architecture. This becomes particularly problematic on newer architectures like ARM64, where native drivers may not be available.
Impact on ORMs
Entity Framework Core
Entity Framework Core, despite its modern design, still relies on database providers that may have native dependencies:
public class MyDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// This configuration depends on:
// 1. SQL Native Client
// 2. Microsoft.Data.SqlClient native components
optionsBuilder.UseSqlServer(connectionString);
}
}
DevExpress XPO
DevExpress XPO faces similar challenges:
// XPO configuration
string connectionString = MSSqlConnectionProvider.GetConnectionString("server", "database");
XpoDefault.DataLayer = XpoDefault.GetDataLayer(connectionString, AutoCreateOption.DatabaseAndSchema);
// The MSSqlConnectionProvider relies on the same native SQL Server components
Solutions and Best Practices
1. Architecture-Specific Deployment
Instead of relying on AnyCPU, consider creating architecture-specific builds:
<PropertyGroup>
<Platforms>x86;x64;arm64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
</PropertyGroup>
2. Runtime Provider Selection
Implement smart provider selection based on the current architecture:
public static class DatabaseProviderFactory
{
public static IDbConnection GetProvider()
{
return RuntimeInformation.ProcessArchitecture switch
{
Architecture.X86 => new SqlConnection(), // x86 native provider
Architecture.X64 => new SqlConnection(), // x64 native provider
Architecture.Arm64 => new Microsoft.Data.SqlClient.SqlConnection(), // ARM64 support
_ => throw new PlatformNotSupportedException()
};
}
}
3. Managed Fallbacks
Implement fallback strategies when native providers aren’t available:
public class DatabaseConnection
{
public async Task<IDbConnection> CreateConnectionAsync()
{
try
{
var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
return connection;
}
catch (DllNotFoundException)
{
var managedConnection = new Microsoft.Data.SqlClient.SqlConnection(_connectionString);
await managedConnection.OpenAsync();
return managedConnection;
}
}
}
4. Deployment Considerations
- Include all necessary native dependencies for each target architecture.
- Use architecture-specific directories in your deployment.
- Consider self-contained deployment to include the correct runtime.
Real-World Implications
This experience taught me that while AnyCPU provides excellent flexibility for managed code, it has limitations when dealing with native dependencies. These limitations become more apparent in scenarios like cloud deployments, ARM64 devices, and live demos.
Conclusion
The transition to ARM architecture is accelerating, and understanding the nuances of AnyCPU and native dependencies is more important than ever. By planning for architecture-specific deployments and implementing fallback strategies, you can build more resilient applications that can thrive in a multi-architecture world.
by Joche Ojeda | Oct 15, 2024 | A.I, Semantic Kernel, XAF, XPO
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:
- 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.
- 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/
by Joche Ojeda | Oct 11, 2024 | A.I, DevExpress, XAF
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!!!
by Joche Ojeda | Oct 10, 2024 | A.I, PropertyEditors, XAF
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!
by Joche Ojeda | Jul 28, 2024 | PropertyEditors, XAF
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
- 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.
- Set Up XAF:
- Follow the DevExpress documentation to set up XAF in both projects. Official documentation here
Step 2: Create a Razor Class Library
- Create a Razor Class Library:
- Add a new Razor Class Library project to the solution.
- Name it XafVsCodeEditor.
- 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
- 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.
- 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.
- Add Required Files:
- wwwroot: folder to host CSS, JavaScript, and the index.html.
- _Imports.razor: this file adds global imports. Source here.
- 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.