Skip to content

(#330) Blazor WASM sample and docs updates. #394

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/prompts/wasm-sample.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
This repository contains an open source project for accessing databases through a client-server relationship with LINQ. You can find many samples in the samples directory, but the main one is todoapp, which contains samples for Avalonia, MAUI, Uno, WinUI3, and WPF. There is also a todoapp-mvc that encapsulates a service and client using JavaScript.

Project Layout:

- `docs` contains full documentation for the library. `docs/in-depth/client/online.md` is a good resource for online client operations.
- `samples` contains a set of samples.
- `src` and `test` contain the source code and tests for the libraries we distribute.

Your job is to create a working sample for Blazor WASM. It should implement a TodoList type application (common with the other samples), encapsulated in a server (see todoapp-mvc for an example server implementation). The solution should be placed in `samples/todoapp-blazor-wasm` and should be runnable with F5-Run. Use an in-memory EF Core store for storing the data (so we aren't reliant on an external database service).
34 changes: 34 additions & 0 deletions docs/in-depth/client/advanced/blazor-wasm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Blazor WASM Support

You can use Blazor WASM with the Datasync Community Toolkit; however, there are some signficant problems:

1. In the general case, you cannot use SQLite for offline storage.
2. You will have to suppress the `WASM0001` warning for sqlite.

## Do not use offline mode

It is possible to use offline mode by storing the SQLite database in local storage within the browser. When you start up your application, you restore the SQLite in-memory database into memory; on a regular basis, you write the SQLite database to local storage. However, this system severely affects performance within the browser and large synchronizations will cause the local storage to overflow. As a result of these problems, it is not recommended, nor is it supported.

## Suppress the `WASM0001` warning

When compiling the WASM client, you will see warning `WASM0001`. This is a harmless warning that indicates SQLite is not available. However, you may be running using "Warnings as Errors". The most appropriate solution is to suppress the `WASM0001` warning in your client `.csproj` file as follows:

```xml
<PropertyGroup>
<NoWarn>$(NoWarn);WASM0001</NoWarn>
</PropertyGroup>
```

Alternatively, you may also use the following to keep `WASM0001` as a warning even when using "Warnings as Errors":

```xml
<PropertyGroup>
<WarningsNotAsErrors>$(WarningsNotAsErrors);WASM0001</WarningsNotAsErrors>
</PropertyGroup>
```

This will suppress the harmless SQLite warning that appears when building Blazor WASM applications that reference libraries containing SQLite dependencies (even when not using SQLite directly).

## Sample

We have a Blazor WASM sample in our sample set: [samples/todoapp-blazor-wasm](https://github.com/CommunityToolkit/Datasync/tree/main/samples/todoapp-blazor-wasm). This sample comprises a server (which uses an in-memory EF Core database) combined with a Blazor Web Assembly client.
4 changes: 2 additions & 2 deletions docs/in-depth/client/index.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Creating offline clients

This guide shows you how to perform common scenarios using the Datasync Community Toolkit. Use the client library in any .NET 8 application, including AvaloniaUI, MAUI, Uno Platform, WinUI, and WPF applications.
This guide shows you how to perform common scenarios using the Datasync Community Toolkit. Use the client library in any .NET 9 application, including AvaloniaUI, MAUI, Uno Platform, WinUI, and WPF applications.

!!! note **Blazor WASM and Blazor Hybrid**
The offline capabilities are known to have issues with Blazor WASM and Blazor Hybrid (since EF Core and SQLite do not work in those environments when running in the browser). Use online-only operations in these environments.
The offline capabilities are known to have issues with Blazor WASM and Blazor Hybrid (since EF Core and SQLite do not work in those environments when running in the browser). Use online-only operations in these environments. For more information, see [our guide on Blazor WASM](./advanced/blazor-wasm.md)

This guide primary deals with offline operations. For online operations, see the [Online operations guide](./online.md).

Expand Down
2 changes: 2 additions & 0 deletions docs/in-depth/client/online.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Not all data needs to be synchronized. You may want to do an online search of records for a search capability, for example. To support this, The Datasync Community Toolkit supports an online client in addition to offline usage.

If you are using the Datasync Community Toolkit with Blazor WASM, see [our guide on Blazor WASM usage](./advanced/blazor-wasm.md).

## Creating a Http Client Factory

To create an online client, you must create an `IHttpClientFactory` that creates the appropriate `HttpClient` objects that are used to communicate with the remote service. This can handle authentication, logging, and anything else that is required by the remote service. At a minimum, a `BaseAddress` must be established. To facilitate this, the Datasync Community Toolkit provides a default `HttpClientFactory` that can be used:
Expand Down
2 changes: 2 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Currently, the library supports:
Client platforms that have been tested include:

* [Avalonia][avalonia]
* [Blazor WASM][blazor-wasm]
* [.NET MAUI][maui]
* [Uno Platform][uno]
* [Windows Presentation Framework (WPF)][wpf]
Expand Down Expand Up @@ -43,6 +44,7 @@ Find out more about developing datasync applications:
[org-github]: https://learn.microsoft.com/en-us/dotnet/communitytoolkit/
[dotnetfdn]: https://dotnetfoundation.org/
[avalonia]: https://www.avaloniaui.net/
[blazor-wasm]: https://learn.microsoft.com/en-us/aspnet/core/blazor/webassembly-build-tools-and-aot
[maui]: https://dotnet.microsoft.com/apps/maui
[uno]: https://platform.uno/
[wpf]: https://learn.microsoft.com/dotnet/desktop/wpf/overview/
Expand Down
12 changes: 12 additions & 0 deletions samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/App.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
@page "/"
@page "/{filter}"
@inject ITodoService TodoService
@inject IJSRuntime JSRuntime

<PageTitle>TodoMVC - Blazor WASM</PageTitle>

<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo"
placeholder="What needs to be done?"
@bind="newTodoTitle"
@onkeypress="HandleNewTodoKeyPress"
autofocus />
</header>

@if (todoItems.Any())
{
<section class="main">
<input id="toggle-all"
class="toggle-all"
type="checkbox"
checked="@allCompleted"
@onchange="ToggleAllTodos" />
<label for="toggle-all">Mark all as complete</label>

<ul class="todo-list">
@foreach (var item in FilteredTodos)
{
<li class="@(item.Completed ? "completed" : "")" @key="item.Id">
<div class="view">
<input class="toggle"
type="checkbox"
checked="@item.Completed"
@onchange="() => ToggleTodo(item)" />
<label @ondblclick="() => StartEditing(item.Id)">@item.Title</label>
<button class="destroy" @onclick="() => DeleteTodo(item.Id)"></button>
</div>
@if (editingTodoId == item.Id)
{
<input class="edit"
@bind="editingTitle"
@onkeypress="(e) => HandleEditKeyPress(e, item)"
@onblur="() => SaveEdit(item)"
@ref="editInput" />
}
</li>
}
</ul>
</section>

<footer class="footer">
<span class="todo-count">
<strong>@activeTodoCount</strong> @(activeTodoCount == 1 ? "item" : "items") left
</span>

<ul class="filters">
<li><a href="/" class="@(CurrentFilter == "all" ? "selected" : "")">All</a></li>
<li><a href="/active" class="@(CurrentFilter == "active" ? "selected" : "")">Active</a></li>
<li><a href="/completed" class="@(CurrentFilter == "completed" ? "selected" : "")">Completed</a></li>
</ul>

@if (completedTodoCount > 0)
{
<button class="clear-completed" @onclick="ClearCompleted">
Clear completed
</button>
}
</footer>
}
</section>

<footer class="info">
<p>Double-click to edit a todo</p>
<p>Created with <a href="https://blazor.net/">Blazor</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>

@code {
[Parameter] public string? Filter { get; set; }

private List<TodoItemDto> todoItems = new();
private string newTodoTitle = string.Empty;
private string? editingTodoId;
private string editingTitle = string.Empty;
private bool allCompleted;
private ElementReference editInput;

private string CurrentFilter => Filter ?? "all";

private IEnumerable<TodoItemDto> FilteredTodos => CurrentFilter switch
{
"active" => todoItems.Where(t => !t.Completed),
"completed" => todoItems.Where(t => t.Completed),
_ => todoItems
};

private int activeTodoCount => todoItems.Count(t => !t.Completed);
private int completedTodoCount => todoItems.Count(t => t.Completed);

protected override async Task OnInitializedAsync()
{
await LoadTodos();
}

private async Task LoadTodos()
{
try
{
var items = await TodoService.GetTodoItemsAsync();
todoItems = items.ToList();
UpdateAllCompletedState();
}
catch (Exception ex)
{
Console.WriteLine($"Error loading todos: {ex.Message}");
}
}

private async Task HandleNewTodoKeyPress(KeyboardEventArgs e)
{
if (e.Key == "Enter" && !string.IsNullOrWhiteSpace(newTodoTitle))
{
await CreateTodo();
}
}

private async Task CreateTodo()
{
try
{
var newItem = await TodoService.CreateTodoItemAsync(newTodoTitle.Trim());
todoItems.Add(newItem);
newTodoTitle = string.Empty;
UpdateAllCompletedState();
}
catch (Exception ex)
{
Console.WriteLine($"Error creating todo: {ex.Message}");
}
}

private async Task UpdateTodo(TodoItemDto item)
{
try
{
await TodoService.UpdateTodoItemAsync(item);
UpdateAllCompletedState();
}
catch (Exception ex)
{
Console.WriteLine($"Error updating todo: {ex.Message}");
}
}

private async Task ToggleTodo(TodoItemDto item)
{
item.Completed = !item.Completed;
await UpdateTodo(item);
}

private async Task DeleteTodo(string id)
{
try
{
await TodoService.DeleteTodoItemAsync(id);
todoItems.RemoveAll(t => t.Id == id);
UpdateAllCompletedState();
}
catch (Exception ex)
{
Console.WriteLine($"Error deleting todo: {ex.Message}");
}
}

private async Task ToggleAllTodos()
{
try
{
foreach (var item in todoItems)
{
item.Completed = allCompleted;
await TodoService.UpdateTodoItemAsync(item);
}
}
catch (Exception ex)
{
Console.WriteLine($"Error toggling all todos: {ex.Message}");
}
}

private async Task ClearCompleted()
{
try
{
var completedItems = todoItems.Where(t => t.Completed).ToList();
foreach (var item in completedItems)
{
await TodoService.DeleteTodoItemAsync(item.Id);
}
todoItems.RemoveAll(t => t.Completed);
UpdateAllCompletedState();
}
catch (Exception ex)
{
Console.WriteLine($"Error clearing completed todos: {ex.Message}");
}
}

private async Task StartEditing(string id)
{
editingTodoId = id;
var item = todoItems.First(t => t.Id == id);
editingTitle = item.Title;

await Task.Delay(1); // Allow DOM to update
await editInput.FocusAsync();
}

private async Task HandleEditKeyPress(KeyboardEventArgs e, TodoItemDto item)
{
if (e.Key == "Enter")
{
await SaveEdit(item);
}
else if (e.Key == "Escape")
{
CancelEdit();
}
}

private async Task SaveEdit(TodoItemDto item)
{
if (!string.IsNullOrWhiteSpace(editingTitle))
{
item.Title = editingTitle.Trim();
await UpdateTodo(item);
}
else
{
await DeleteTodo(item.Id);
}

editingTodoId = null;
editingTitle = string.Empty;
}

private void CancelEdit()
{
editingTodoId = null;
editingTitle = string.Empty;
}

private void UpdateAllCompletedState()
{
allCompleted = todoItems.Any() && todoItems.All(t => t.Completed);
}
}
33 changes: 33 additions & 0 deletions samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using CommunityToolkit.Datasync.Client;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using TodoApp.BlazorWasm.Client;
using TodoApp.BlazorWasm.Client.Services;
using TodoApp.BlazorWasm.Shared.Models;

WebAssemblyHostBuilder builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

// Configure HTTP client for Datasync
builder.Services.AddHttpClient("DatasyncClient", client =>
{
client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
});

// Register Datasync services
builder.Services.AddScoped<DatasyncServiceClient<TodoItemDto>>(sp =>
{
IHttpClientFactory httpClientFactory = sp.GetRequiredService<IHttpClientFactory>();
HttpClient httpClient = httpClientFactory.CreateClient("DatasyncClient");
Uri tableEndpoint = new("/tables/todoitems", UriKind.Relative);
return new DatasyncServiceClient<TodoItemDto>(tableEndpoint, httpClient);
});

builder.Services.AddScoped<ITodoService, TodoService>();

await builder.Build().RunAsync();
Loading