Set XAF Blazor ViewItems Free: Direct URLs, Parameters, and a REST API

XAF gives you an enormous amount for free — security, CRUD, navigation, a polished Blazor UI
generated from your business model. But everything it generates lives inside the
application. A ViewItem — your custom piece of UI — is reachable only the XAF way: log in,
navigate the menu, open the view. You can't send someone a link straight to it. You can't put
?orderId=123 in the URL. You can't POST to it from a mobile app or a Telegram bot.
For internal line-of-business screens that's fine. But the moment you want to share a view,
embed it, or call it programmatically, that wall gets in the way.
This post is the write-up of egarim/XafBlazorViewItemUrlConfiguration,
a reference project that tears the wall down. The trick is simple to state and very reusable:
Build the ViewItem's UI as a plain Blazor component, then surface that one component three
ways — as the traditional XAF ViewItem, as a routable standalone page with URL parameters,
and as a REST API endpoint.
The demo uses an AI chat ViewItem (DevExpress DxAIChat backed by the GitHub Copilot SDK),
but the pattern works for any ViewItem: order details, customer profiles, reports,
dashboard widgets.
The shape of the problem
| Traditional XAF ViewItem | What we want | |
|---|---|---|
| Direct URL access | ❌ login + navigate | ✅ /chat |
| URL parameters | ❌ none | ✅ /chat?message=Hello&context=order123 |
| Shareable / bookmarkable | ❌ no | ✅ yes (links, QR codes, email buttons) |
| Embeddable in another app | ❌ no | ✅ yes |
| Programmatic / REST | ❌ no | ✅ POST /api/chat |
| XAF security | ✅ full | ⚙️ optional, per surface |
The keystone: the UI is just a component
Everything depends on this one decision. Instead of writing chat logic inside a ViewItem
class, you write it as an ordinary, self-contained Razor component — CopilotChat.razor — that
knows nothing about XAF. It renders the DxAIChat, wires the system prompt, and routes
messages through whatever IChatClient is registered in DI:
<div class="copilot-chat-container">
<DxAIChat CssClass="copilot-chat"
UseStreaming="true"
ResponseContentFormat="ResponseContentFormat.Markdown"
Initialized="OnChatInitialized">
<PromptSuggestions>
@foreach (var s in CopilotChatDefaults.PromptSuggestions)
{
<DxAIChatPromptSuggestion Title="@s.Title" Text="@s.Text" PromptMessage="@s.Prompt" />
}
</PromptSuggestions>
</DxAIChat>
</div>
@code {
void OnChatInitialized(IAIChat chat)
=> chat.LoadMessages(new[] { new BlazorChatMessage(ChatRole.System, CopilotChatDefaults.SystemPrompt) });
}
Because this component is framework-agnostic, it can be hosted by anything that renders Blazor.
That's the whole game. Now we give it three homes.
Surface 1 — the traditional XAF ViewItem
Inside XAF, you wrap the component in a ViewItem that implements IComponentContentHolder.
This is the supported XAF-Blazor seam for "render this Razor component as my view item." The
ViewItem does almost nothing except hand XAF the component:
[ViewItem(typeof(IModelCopilotChatViewItem))]
public class CopilotChatViewItemBlazor : ViewItem, IComponentContentHolder
{
public CopilotChatViewItemBlazor(IModelViewItem model, Type objectType)
: base(objectType, model.Id) { }
RenderFragment IComponentContentHolder.ComponentContent => builder =>
{
builder.OpenComponent<CopilotChat>(0); // ← the same shared component
builder.CloseComponent();
};
protected override object CreateControlCore() => new object();
// In Blazor, IComponentContentHolder.ComponentContent does the rendering;
// CreateControlCore just satisfies the ViewItem contract.
}
This is the "normal" experience: full XAF security, sitting in a DetailView, reached by logging
in and navigating. Nothing new for your users — but the UI is now reusable.
Surface 2 — the URL-addressable page (the main event)
Here's where the ViewItem escapes. A standalone Blazor page uses @page routes and Blazor's
[SupplyParameterFromQuery] to read URL parameters, then drives the same component with
them:
@page "/chat"
@page "/chat/{InitialMessage?}"
<DxAIChat ... Initialized="OnChatInitialized" />
@code {
[Parameter, SupplyParameterFromQuery(Name = "message")]
public string? InitialMessage { get; set; }
[Parameter, SupplyParameterFromQuery(Name = "context")]
public string? Context { get; set; }
void OnChatInitialized(IAIChat chat)
{
var systemPrompt = CopilotChatDefaults.SystemPrompt;
// URL parameter → ViewItem state
if (!string.IsNullOrEmpty(Context))
systemPrompt += quot;\n\nContext: {Context}";
chat.LoadMessages(new[] { new BlazorChatMessage(ChatRole.System, systemPrompt) });
// ?message=… auto-sends a first message once the chat is ready
if (!string.IsNullOrEmpty(InitialMessage))
_ = Task.Run(async () =>
{
await Task.Delay(500);
await InvokeAsync(() => chat.SendMessageAsync(InitialMessage));
});
}
}
That's all it takes to turn a buried view into a linkable surface:
/chat
/chat?message=Hello
/chat?message=Show me order 123&context=order123
The two query parameters do real work: message pre-populates and auto-sends the opening
message; context is folded into the system prompt so the AI knows which order/user/record
the link is about. Swap the chat for an order view and you've got /order?id=123 — a link you
can drop in an email, behind a QR code on a receipt, or into another app's <iframe>.
The key insight: URL parameters become ViewItem state. The page is a thin adapter that
translates?key=valueinto the same configuration you'd otherwise set by hand inside XAF.
Surface 3 — the REST API (bonus)
The same capability, minus the UI, for machines. A plain ASP.NET Core controller takes the
injected IChatClient and exposes it over HTTP — perfect for mobile apps, automations, or a
Discord/Telegram bot:
[ApiController]
[Route("api/[controller]")]
public class ChatController : ControllerBase
{
private readonly IChatClient _chatClient;
public ChatController(IChatClient chatClient, ILogger<ChatController> logger) { _chatClient = chatClient; ... }
[HttpPost]
public async Task<IActionResult> SendMessage([FromBody] ChatRequest request)
{
if (string.IsNullOrWhiteSpace(request.Message))
return BadRequest(new { error = "Message is required" });
var messages = new List<ChatMessage> { new(ChatRole.System, BuildSystemPrompt(request.Context)) };
messages.Add(new(ChatRole.User, request.Message));
var response = await _chatClient.CompleteAsync(messages);
return Ok(new ChatResponse {
Message = response.Message.Text ?? string.Empty,
Model = response.ModelId ?? "unknown",
TokensUsed = response.Usage?.TotalTokenCount ?? 0,
Timestamp = DateTime.UtcNow
});
}
}
curl -X POST https://localhost:5002/api/chat \
-H "Content-Type: application/json" \
-d '{"message": "List all orders", "context": {"userId": "123"}}'
Notice that the controller and the Blazor page share the exact same building blocks — the
IChatClient from DI and a BuildSystemPrompt(context) helper. The business logic lives in
one place; the three surfaces are just different doors into it.
One component, three doors
CopilotChat.razor + IChatClient (DI) ← the single source of truth
│
┌──────────────┼───────────────────────┐
▼ ▼ ▼
XAF ViewItem /chat?message=… POST /api/chat
(full security) (shareable URL + params) (programmatic JSON)
That diagram is the entire idea. You write the view once, and you decide per surface how much
XAF you want around it.
Security is a spectrum, not a switch
Freeing a view from the login wall doesn't mean throwing security away — it means choosing it
deliberately for each door. The project lays out the options:
// Fully public standalone page
@page "/chat"
// Require an authenticated user
@page "/chat"
@attribute [Authorize]
// API-key-protected endpoint
[ApiKey] // custom attribute
public class ChatController : ControllerBase { ... }
// Or branch: XAF security when logged in, limited access otherwise
if (User.Identity?.IsAuthenticated == true) { /* full */ } else { /* public subset */ }
A shareable order link probably wants a signed token in the query string; an internal dashboard
page wants [Authorize]; the REST endpoint wants an API key. The point is that you get to
pick, instead of the framework picking "always full login" for you.
Why this is worth knowing
XAF's productivity comes from generating a complete, secured application. The cost is that the
pieces it generates are, by default, only reachable from inside that application. This pattern
buys back the flexibility without giving up the productivity:
- Reuse — the ViewItem UI becomes an ordinary Blazor component you can host anywhere.
- Reach — direct URLs make views shareable, bookmarkable, embeddable, QR-codeable.
- Configurability — URL parameters drive view state, no code change per link.
- Interoperability — a REST surface lets mobile apps, bots, and external systems in.
- Choice — security is decided per surface, not imposed globally.
The demo happens to be an AI chat, but mentally replace it with whatever screen your users keep
asking you to "just send me a link to." Clone
egarim/XafBlazorViewItemUrlConfiguration,
run the Blazor server and the Web API, and try /chat?message=Hello for yourself.
Questions, or a ViewItem you'd love to expose this way? Find me on the links on the
about page.