Skip to content

Commit d502b80

Browse files
authored
(#21) WPF Sample and docs (#94)
* (#21) Reset of WPF sample * (#21) Initial version of the WPF application. * (#21) WPF Sample and docs
1 parent 001a31b commit d502b80

File tree

20 files changed

+294
-290
lines changed

20 files changed

+294
-290
lines changed

docs/content/samples/todoapp/wpf.md

Lines changed: 64 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,71 @@
22
title = "WPF"
33
+++
44

5-
You can find [our sample TodoApp for WPF](https://github.com/CommunityToolkit/Datasync/tree/main/samples/todoapp/TodoApp.WPF) on our GitHub repository. All of our logic has been placed in the `Database/AppDbContext.cs` file:
6-
7-
{{< highlight lineNos="true" type="csharp" wrap="true" title="AppDbContext.cs" >}}
8-
public class AppDbContext(DbContextOptions<AppDbContext> options) : OfflineDbContext(options)
9-
{
10-
public DbSet<TodoItem> TodoItems => Set<TodoItem>();
11-
12-
protected override void OnDatasyncInitialization(DatasyncOfflineOptionsBuilder optionsBuilder)
13-
{
14-
HttpClientOptions clientOptions = new()
15-
{
16-
Endpoint = new Uri("https://YOURSITEHERE.azurewebsites.net/"),
17-
HttpPipeline = [new LoggingHandler()]
18-
};
19-
_ = optionsBuilder.UseHttpClientOptions(clientOptions);
20-
}
5+
## Run the application first
216

22-
public async Task SynchronizeAsync(CancellationToken cancellationToken = default)
23-
{
24-
PushResult pushResult = await this.PushAsync(cancellationToken);
25-
if (!pushResult.IsSuccessful)
26-
{
27-
throw new ApplicationException($"Push failed: {pushResult.FailedRequests.FirstOrDefault().Value.ReasonPhrase}");
28-
}
29-
30-
PullResult pullResult = await this.PullAsync(cancellationToken);
31-
if (!pullResult.IsSuccessful)
32-
{
33-
throw new ApplicationException($"Pull failed: {pullResult.FailedRequests.FirstOrDefault().Value.ReasonPhrase}");
34-
}
35-
}
36-
}
37-
{{< /highlight >}}
7+
The WPF sample uses an in-memory Sqlite store for storing its data. To run the application locally:
8+
9+
* [Configure Visual Studio for WPF development](https://learn.microsoft.com/visualstudio/get-started/csharp/tutorial-wpf).
10+
* Open `samples/todoapp/Samples.TodoApp.sln` in Visual Studio.
11+
* In the Solution Explorer, right-click the `TodoApp.WPF` project, then select **Set as Startup Project**.
12+
* Select a target (in the top bar), then press F5 to run the application.
13+
14+
If you bump into issues at this point, ensure you can properly develop and run WPF applications outside of the datasync service.
15+
16+
## Deploy a datasync server to Azure
17+
18+
Before you begin adjusting the application for offline usage, you must [deploy a datasync service](../server.md). Make a note of the URI of the service before continuing.
19+
20+
## Update the application for datasync operations
21+
22+
All the changes are isolated to the `Database/AppDbContext.cs` file.
23+
24+
1. Change the definition of the class so that it inherits from `OfflineDbContext`:
3825

39-
To enable offline synchronization:
26+
```csharp
27+
public class AppDbContext(DbContextOptions<AppDbContext> options) : OfflineDbContext(options)
28+
{
29+
// Rest of the class
30+
}
31+
```
32+
33+
2. Add the `OnDatasyncInitialization()` method:
34+
35+
```csharp
36+
protected override void OnDatasyncInitialization(DatasyncOfflineOptionsBuilder optionsBuilder)
37+
{
38+
HttpClientOptions clientOptions = new()
39+
{
40+
Endpoint = new Uri("https://YOURSITEHERE.azurewebsites.net/"),
41+
HttpPipeline = [new LoggingHandler()]
42+
};
43+
_ = optionsBuilder.UseHttpClientOptions(clientOptions);
44+
}
45+
```
46+
47+
Replace the Endpoint with the URI of your datasync service.
48+
49+
3. Update the `SynchronizeAsync()` method.
50+
51+
The `SynchronizeAsync()` method is used by the application to synchronize data to and from the datasync service. It is called primarily from the `MainViewModel` which drives the UI interactions for the main list.
52+
53+
```csharp
54+
public async Task SynchronizeAsync(CancellationToken cancellationToken = default)
55+
{
56+
PushResult pushResult = await this.PushAsync(cancellationToken);
57+
if (!pushResult.IsSuccessful)
58+
{
59+
throw new ApplicationException($"Push failed: {pushResult.FailedRequests.FirstOrDefault().Value.ReasonPhrase}");
60+
}
61+
62+
PullResult pullResult = await this.PullAsync(cancellationToken);
63+
if (!pullResult.IsSuccessful)
64+
{
65+
throw new ApplicationException($"Pull failed: {pullResult.FailedRequests.FirstOrDefault().Value.ReasonPhrase}");
66+
}
67+
}
68+
```
4069

41-
* Switch from `DbContext` to `OfflineDbContext`.
42-
* Define your `OnDatasyncInitialization()` method (don't forget to change the URL to the URL of your datasync server).
43-
* Where appropriate, use `PushAsync()` and `PullAsync()` to communicate with the server.
70+
You can now re-run your application. Watch the console logs to show the interactions with the datasync service. Press the refresh button to synchronize data with the cloud. When you restart the application, your changes will automatically populate the database again.
4471

45-
We have placed a `SynchronizeAsync()` method on the database context, which is used in the view model for the single page we have.
72+
Obviously, you will want to do much more in a "real world" application, including proper error handling, authentication, and using a Sqlite file instead of an in-memory database. This example shows off the minimum required to add datasync services to an application.

samples/todoapp/Samples.TodoApp.sln

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApp.WinUI3", "TodoApp.W
99
EndProject
1010
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Datasync.Client", "..\..\src\CommunityToolkit.Datasync.Client\CommunityToolkit.Datasync.Client.csproj", "{2AC73FBE-9E76-4702-B551-B5884383CC68}"
1111
EndProject
12-
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApp.WPF", "TodoApp.WPF\TodoApp.WPF.csproj", "{410D4BBD-5ED7-4BC0-A2CF-547A4784732F}"
13-
EndProject
1412
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Datasync.Server", "..\datasync-server\src\Sample.Datasync.Server\Sample.Datasync.Server.csproj", "{E67734DD-B397-4A65-AA50-D62F37EF05DD}"
1513
EndProject
16-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoApp.MAUI", "TodoApp.MAUI\TodoApp.MAUI.csproj", "{00430043-04C5-4F8F-87A9-98ECC0051808}"
14+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApp.MAUI", "TodoApp.MAUI\TodoApp.MAUI.csproj", "{00430043-04C5-4F8F-87A9-98ECC0051808}"
15+
EndProject
16+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoApp.WPF", "TodoApp.WPF\TodoApp.WPF.csproj", "{A0996FB8-890D-4E90-A881-01F9EF709711}"
1717
EndProject
1818
Global
1919
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -67,22 +67,6 @@ Global
6767
{2AC73FBE-9E76-4702-B551-B5884383CC68}.Release|x64.Build.0 = Release|Any CPU
6868
{2AC73FBE-9E76-4702-B551-B5884383CC68}.Release|x86.ActiveCfg = Release|Any CPU
6969
{2AC73FBE-9E76-4702-B551-B5884383CC68}.Release|x86.Build.0 = Release|Any CPU
70-
{410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
71-
{410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|Any CPU.Build.0 = Debug|Any CPU
72-
{410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|ARM64.ActiveCfg = Debug|Any CPU
73-
{410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|ARM64.Build.0 = Debug|Any CPU
74-
{410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|x64.ActiveCfg = Debug|Any CPU
75-
{410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|x64.Build.0 = Debug|Any CPU
76-
{410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|x86.ActiveCfg = Debug|Any CPU
77-
{410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|x86.Build.0 = Debug|Any CPU
78-
{410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|Any CPU.ActiveCfg = Release|Any CPU
79-
{410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|Any CPU.Build.0 = Release|Any CPU
80-
{410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|ARM64.ActiveCfg = Release|Any CPU
81-
{410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|ARM64.Build.0 = Release|Any CPU
82-
{410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|x64.ActiveCfg = Release|Any CPU
83-
{410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|x64.Build.0 = Release|Any CPU
84-
{410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|x86.ActiveCfg = Release|Any CPU
85-
{410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|x86.Build.0 = Release|Any CPU
8670
{E67734DD-B397-4A65-AA50-D62F37EF05DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
8771
{E67734DD-B397-4A65-AA50-D62F37EF05DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
8872
{E67734DD-B397-4A65-AA50-D62F37EF05DD}.Debug|ARM64.ActiveCfg = Debug|Any CPU
@@ -123,6 +107,22 @@ Global
123107
{00430043-04C5-4F8F-87A9-98ECC0051808}.Release|x86.ActiveCfg = Release|Any CPU
124108
{00430043-04C5-4F8F-87A9-98ECC0051808}.Release|x86.Build.0 = Release|Any CPU
125109
{00430043-04C5-4F8F-87A9-98ECC0051808}.Release|x86.Deploy.0 = Release|Any CPU
110+
{A0996FB8-890D-4E90-A881-01F9EF709711}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
111+
{A0996FB8-890D-4E90-A881-01F9EF709711}.Debug|Any CPU.Build.0 = Debug|Any CPU
112+
{A0996FB8-890D-4E90-A881-01F9EF709711}.Debug|ARM64.ActiveCfg = Debug|Any CPU
113+
{A0996FB8-890D-4E90-A881-01F9EF709711}.Debug|ARM64.Build.0 = Debug|Any CPU
114+
{A0996FB8-890D-4E90-A881-01F9EF709711}.Debug|x64.ActiveCfg = Debug|Any CPU
115+
{A0996FB8-890D-4E90-A881-01F9EF709711}.Debug|x64.Build.0 = Debug|Any CPU
116+
{A0996FB8-890D-4E90-A881-01F9EF709711}.Debug|x86.ActiveCfg = Debug|Any CPU
117+
{A0996FB8-890D-4E90-A881-01F9EF709711}.Debug|x86.Build.0 = Debug|Any CPU
118+
{A0996FB8-890D-4E90-A881-01F9EF709711}.Release|Any CPU.ActiveCfg = Release|Any CPU
119+
{A0996FB8-890D-4E90-A881-01F9EF709711}.Release|Any CPU.Build.0 = Release|Any CPU
120+
{A0996FB8-890D-4E90-A881-01F9EF709711}.Release|ARM64.ActiveCfg = Release|Any CPU
121+
{A0996FB8-890D-4E90-A881-01F9EF709711}.Release|ARM64.Build.0 = Release|Any CPU
122+
{A0996FB8-890D-4E90-A881-01F9EF709711}.Release|x64.ActiveCfg = Release|Any CPU
123+
{A0996FB8-890D-4E90-A881-01F9EF709711}.Release|x64.Build.0 = Release|Any CPU
124+
{A0996FB8-890D-4E90-A881-01F9EF709711}.Release|x86.ActiveCfg = Release|Any CPU
125+
{A0996FB8-890D-4E90-A881-01F9EF709711}.Release|x86.Build.0 = Release|Any CPU
126126
EndGlobalSection
127127
GlobalSection(SolutionProperties) = preSolution
128128
HideSolutionNode = FALSE

samples/todoapp/TodoApp.MAUI/MainPage.xaml.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ public void OnListItemTapped(object sender, ItemTappedEventArgs e)
2929
{
3030
if (e.Item is TodoItem item)
3131
{
32-
Debug.WriteLine($"[UI] >>> Item clicked: {item.Id}");
3332
this._viewModel.SelectItemCommand.Execute(item);
3433
}
3534

samples/todoapp/TodoApp.WPF/App.xaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
<Application x:Class="TodoApp.WPF.App"
1+
<Application x:Class="TodoApp.WPF.App"
22
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
33
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
44
xmlns:local="clr-namespace:TodoApp.WPF"
55
StartupUri="MainWindow.xaml">
66
<Application.Resources>
7-
87
</Application.Resources>
98
</Application>

samples/todoapp/TodoApp.WPF/App.xaml.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Microsoft.Extensions.DependencyInjection;
88
using System.Windows;
99
using TodoApp.WPF.Database;
10+
using TodoApp.WPF.Services;
1011
using TodoApp.WPF.ViewModels;
1112

1213
namespace TodoApp.WPF;
@@ -17,22 +18,18 @@ namespace TodoApp.WPF;
1718
public partial class App : Application, IDisposable
1819
{
1920
private readonly SqliteConnection dbConnection;
20-
21-
/// <summary>
22-
/// The IoC service provider
23-
/// </summary>
2421
public IServiceProvider Services { get; }
2522

2623
public App()
2724
{
28-
// Create the connection to the SQLite database
2925
this.dbConnection = new SqliteConnection("Data Source=:memory:");
3026
this.dbConnection.Open();
3127

3228
// Create the IoC Services provider.
3329
Services = new ServiceCollection()
3430
.AddTransient<TodoListViewModel>()
3531
.AddScoped<IDbInitializer, DbContextInitializer>()
32+
.AddScoped<IAlertService, AlertService>()
3633
.AddDbContext<AppDbContext>(options => options.UseSqlite(this.dbConnection))
3734
.BuildServiceProvider();
3835

@@ -42,7 +39,6 @@ public App()
4239

4340
private void InitializeDatabase()
4441
{
45-
// using IServiceScope scope = Ioc.Default.CreateScope();
4642
using IServiceScope scope = Services.CreateScope();
4743
IDbInitializer initializer = scope.ServiceProvider.GetRequiredService<IDbInitializer>();
4844
initializer.Initialize();
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Globalization;
6+
using System.Windows.Data;
7+
using System.Windows.Media.Imaging;
8+
9+
namespace TodoApp.WPF.Converters;
10+
11+
/// <summary>
12+
/// A converter to convert the boolean for IsComplete into one of two images.
13+
/// </summary>
14+
public class BooleanToImageConverter : IValueConverter
15+
{
16+
private const string baseUri = "pack://application:,,,/Images";
17+
18+
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
19+
{
20+
if (value is bool booleanValue)
21+
{
22+
return new BitmapImage(new Uri(booleanValue ? $"{baseUri}/completed.png" : $"{baseUri}/incomplete.png"));
23+
}
24+
25+
return null;
26+
}
27+
28+
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
29+
{
30+
throw new NotImplementedException();
31+
}
32+
}

samples/todoapp/TodoApp.WPF/Database/AppDbContext.cs

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,36 @@
55
using CommunityToolkit.Datasync.Client.Http;
66
using CommunityToolkit.Datasync.Client.Offline;
77
using Microsoft.EntityFrameworkCore;
8-
using TodoApp.WPF.Services;
98

109
namespace TodoApp.WPF.Database;
1110

1211
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
1312
{
1413
public DbSet<TodoItem> TodoItems => Set<TodoItem>();
1514

16-
// protected override void OnDatasyncInitialization(DatasyncOfflineOptionsBuilder optionsBuilder)
17-
// {
18-
// HttpClientOptions clientOptions = new()
19-
// {
20-
// Endpoint = new Uri("https://Y.azurewebsites.net/"),
21-
// HttpPipeline = [new LoggingHandler()]
22-
// };
23-
// _ = optionsBuilder.UseHttpClientOptions(clientOptions);
24-
// }
15+
//protected override void OnDatasyncInitialization(DatasyncOfflineOptionsBuilder optionsBuilder)
16+
//{
17+
// HttpClientOptions clientOptions = new()
18+
// {
19+
// Endpoint = new Uri("https://YOURSITEHERE.azurewebsites.net/"),
20+
// HttpPipeline = [new LoggingHandler()]
21+
// };
22+
// _ = optionsBuilder.UseHttpClientOptions(clientOptions);
23+
//}
2524

2625
public async Task SynchronizeAsync(CancellationToken cancellationToken = default)
2726
{
28-
// PushResult pushResult = await this.PushAsync(cancellationToken);
29-
// if (!pushResult.IsSuccessful)
30-
// {
31-
// throw new ApplicationException($"Push failed: {pushResult.FailedRequests.FirstOrDefault().Value.ReasonPhrase}");
32-
// }
33-
34-
// PullResult pullResult = await this.PullAsync(cancellationToken);
35-
// if (!pullResult.IsSuccessful)
36-
// {
37-
// throw new ApplicationException($"Pull failed: {pullResult.FailedRequests.FirstOrDefault().Value.ReasonPhrase}");
38-
// }
27+
//PushResult pushResult = await this.PushAsync(cancellationToken);
28+
//if (!pushResult.IsSuccessful)
29+
//{
30+
// throw new ApplicationException($"Push failed: {pushResult.FailedRequests.FirstOrDefault().Value.ReasonPhrase}");
31+
//}
32+
33+
//PullResult pullResult = await this.PullAsync(cancellationToken);
34+
//if (!pullResult.IsSuccessful)
35+
//{
36+
// throw new ApplicationException($"Pull failed: {pullResult.FailedRequests.FirstOrDefault().Value.ReasonPhrase}");
37+
//}
3938
}
4039
}
4140

@@ -49,7 +48,10 @@ public class DbContextInitializer(AppDbContext context) : IDbInitializer
4948
{
5049
/// <inheritdoc />
5150
public void Initialize()
52-
=> context.Database.EnsureCreated();
51+
{
52+
_ = context.Database.EnsureCreated();
53+
// Task.Run(async () => await context.SynchronizeAsync());
54+
}
5355

5456
/// <inheritdoc />
5557
public Task InitializeAsync(CancellationToken cancellationToken = default)

samples/todoapp/TodoApp.WPF/Database/TodoItem.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,9 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
using System.Text.Json;
6-
75
namespace TodoApp.WPF.Database;
8-
96
public class TodoItem : OfflineClientEntity
107
{
118
public string Title { get; set; } = string.Empty;
129
public bool IsComplete { get; set; } = false;
13-
14-
public override string ToString()
15-
=> JsonSerializer.Serialize(this);
16-
}
10+
}
-373 Bytes
Binary file not shown.
-866 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)