diff --git a/docs/content/samples/todoapp/wpf.md b/docs/content/samples/todoapp/wpf.md index cb1d7acd..bd911fac 100644 --- a/docs/content/samples/todoapp/wpf.md +++ b/docs/content/samples/todoapp/wpf.md @@ -2,44 +2,71 @@ title = "WPF" +++ -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: - -{{< highlight lineNos="true" type="csharp" wrap="true" title="AppDbContext.cs" >}} -public class AppDbContext(DbContextOptions options) : OfflineDbContext(options) -{ - public DbSet TodoItems => Set(); - - protected override void OnDatasyncInitialization(DatasyncOfflineOptionsBuilder optionsBuilder) - { - HttpClientOptions clientOptions = new() - { - Endpoint = new Uri("https://YOURSITEHERE.azurewebsites.net/"), - HttpPipeline = [new LoggingHandler()] - }; - _ = optionsBuilder.UseHttpClientOptions(clientOptions); - } +## Run the application first - public async Task SynchronizeAsync(CancellationToken cancellationToken = default) - { - PushResult pushResult = await this.PushAsync(cancellationToken); - if (!pushResult.IsSuccessful) - { - throw new ApplicationException($"Push failed: {pushResult.FailedRequests.FirstOrDefault().Value.ReasonPhrase}"); - } - - PullResult pullResult = await this.PullAsync(cancellationToken); - if (!pullResult.IsSuccessful) - { - throw new ApplicationException($"Pull failed: {pullResult.FailedRequests.FirstOrDefault().Value.ReasonPhrase}"); - } - } -} -{{< /highlight >}} +The WPF sample uses an in-memory Sqlite store for storing its data. To run the application locally: + +* [Configure Visual Studio for WPF development](https://learn.microsoft.com/visualstudio/get-started/csharp/tutorial-wpf). +* Open `samples/todoapp/Samples.TodoApp.sln` in Visual Studio. +* In the Solution Explorer, right-click the `TodoApp.WPF` project, then select **Set as Startup Project**. +* Select a target (in the top bar), then press F5 to run the application. + +If you bump into issues at this point, ensure you can properly develop and run WPF applications outside of the datasync service. + +## Deploy a datasync server to Azure + +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. + +## Update the application for datasync operations + +All the changes are isolated to the `Database/AppDbContext.cs` file. + +1. Change the definition of the class so that it inherits from `OfflineDbContext`: -To enable offline synchronization: + ```csharp + public class AppDbContext(DbContextOptions options) : OfflineDbContext(options) + { + // Rest of the class + } + ``` + +2. Add the `OnDatasyncInitialization()` method: + + ```csharp + protected override void OnDatasyncInitialization(DatasyncOfflineOptionsBuilder optionsBuilder) + { + HttpClientOptions clientOptions = new() + { + Endpoint = new Uri("https://YOURSITEHERE.azurewebsites.net/"), + HttpPipeline = [new LoggingHandler()] + }; + _ = optionsBuilder.UseHttpClientOptions(clientOptions); + } + ``` + + Replace the Endpoint with the URI of your datasync service. + +3. Update the `SynchronizeAsync()` method. + + 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. + + ```csharp + public async Task SynchronizeAsync(CancellationToken cancellationToken = default) + { + PushResult pushResult = await this.PushAsync(cancellationToken); + if (!pushResult.IsSuccessful) + { + throw new ApplicationException($"Push failed: {pushResult.FailedRequests.FirstOrDefault().Value.ReasonPhrase}"); + } + + PullResult pullResult = await this.PullAsync(cancellationToken); + if (!pullResult.IsSuccessful) + { + throw new ApplicationException($"Pull failed: {pullResult.FailedRequests.FirstOrDefault().Value.ReasonPhrase}"); + } + } + ``` -* Switch from `DbContext` to `OfflineDbContext`. -* Define your `OnDatasyncInitialization()` method (don't forget to change the URL to the URL of your datasync server). -* Where appropriate, use `PushAsync()` and `PullAsync()` to communicate with the server. +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. -We have placed a `SynchronizeAsync()` method on the database context, which is used in the view model for the single page we have. +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. diff --git a/samples/todoapp/Samples.TodoApp.sln b/samples/todoapp/Samples.TodoApp.sln index 69781c3a..1204e402 100644 --- a/samples/todoapp/Samples.TodoApp.sln +++ b/samples/todoapp/Samples.TodoApp.sln @@ -9,11 +9,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApp.WinUI3", "TodoApp.W EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Datasync.Client", "..\..\src\CommunityToolkit.Datasync.Client\CommunityToolkit.Datasync.Client.csproj", "{2AC73FBE-9E76-4702-B551-B5884383CC68}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApp.WPF", "TodoApp.WPF\TodoApp.WPF.csproj", "{410D4BBD-5ED7-4BC0-A2CF-547A4784732F}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Datasync.Server", "..\datasync-server\src\Sample.Datasync.Server\Sample.Datasync.Server.csproj", "{E67734DD-B397-4A65-AA50-D62F37EF05DD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoApp.MAUI", "TodoApp.MAUI\TodoApp.MAUI.csproj", "{00430043-04C5-4F8F-87A9-98ECC0051808}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApp.MAUI", "TodoApp.MAUI\TodoApp.MAUI.csproj", "{00430043-04C5-4F8F-87A9-98ECC0051808}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoApp.WPF", "TodoApp.WPF\TodoApp.WPF.csproj", "{A0996FB8-890D-4E90-A881-01F9EF709711}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -67,22 +67,6 @@ Global {2AC73FBE-9E76-4702-B551-B5884383CC68}.Release|x64.Build.0 = Release|Any CPU {2AC73FBE-9E76-4702-B551-B5884383CC68}.Release|x86.ActiveCfg = Release|Any CPU {2AC73FBE-9E76-4702-B551-B5884383CC68}.Release|x86.Build.0 = Release|Any CPU - {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|ARM64.ActiveCfg = Debug|Any CPU - {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|ARM64.Build.0 = Debug|Any CPU - {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|x64.ActiveCfg = Debug|Any CPU - {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|x64.Build.0 = Debug|Any CPU - {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|x86.ActiveCfg = Debug|Any CPU - {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|x86.Build.0 = Debug|Any CPU - {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|Any CPU.Build.0 = Release|Any CPU - {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|ARM64.ActiveCfg = Release|Any CPU - {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|ARM64.Build.0 = Release|Any CPU - {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|x64.ActiveCfg = Release|Any CPU - {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|x64.Build.0 = Release|Any CPU - {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|x86.ActiveCfg = Release|Any CPU - {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|x86.Build.0 = Release|Any CPU {E67734DD-B397-4A65-AA50-D62F37EF05DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E67734DD-B397-4A65-AA50-D62F37EF05DD}.Debug|Any CPU.Build.0 = Debug|Any CPU {E67734DD-B397-4A65-AA50-D62F37EF05DD}.Debug|ARM64.ActiveCfg = Debug|Any CPU @@ -123,6 +107,22 @@ Global {00430043-04C5-4F8F-87A9-98ECC0051808}.Release|x86.ActiveCfg = Release|Any CPU {00430043-04C5-4F8F-87A9-98ECC0051808}.Release|x86.Build.0 = Release|Any CPU {00430043-04C5-4F8F-87A9-98ECC0051808}.Release|x86.Deploy.0 = Release|Any CPU + {A0996FB8-890D-4E90-A881-01F9EF709711}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A0996FB8-890D-4E90-A881-01F9EF709711}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A0996FB8-890D-4E90-A881-01F9EF709711}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {A0996FB8-890D-4E90-A881-01F9EF709711}.Debug|ARM64.Build.0 = Debug|Any CPU + {A0996FB8-890D-4E90-A881-01F9EF709711}.Debug|x64.ActiveCfg = Debug|Any CPU + {A0996FB8-890D-4E90-A881-01F9EF709711}.Debug|x64.Build.0 = Debug|Any CPU + {A0996FB8-890D-4E90-A881-01F9EF709711}.Debug|x86.ActiveCfg = Debug|Any CPU + {A0996FB8-890D-4E90-A881-01F9EF709711}.Debug|x86.Build.0 = Debug|Any CPU + {A0996FB8-890D-4E90-A881-01F9EF709711}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A0996FB8-890D-4E90-A881-01F9EF709711}.Release|Any CPU.Build.0 = Release|Any CPU + {A0996FB8-890D-4E90-A881-01F9EF709711}.Release|ARM64.ActiveCfg = Release|Any CPU + {A0996FB8-890D-4E90-A881-01F9EF709711}.Release|ARM64.Build.0 = Release|Any CPU + {A0996FB8-890D-4E90-A881-01F9EF709711}.Release|x64.ActiveCfg = Release|Any CPU + {A0996FB8-890D-4E90-A881-01F9EF709711}.Release|x64.Build.0 = Release|Any CPU + {A0996FB8-890D-4E90-A881-01F9EF709711}.Release|x86.ActiveCfg = Release|Any CPU + {A0996FB8-890D-4E90-A881-01F9EF709711}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/samples/todoapp/TodoApp.MAUI/MainPage.xaml.cs b/samples/todoapp/TodoApp.MAUI/MainPage.xaml.cs index e6a84b10..496fed08 100644 --- a/samples/todoapp/TodoApp.MAUI/MainPage.xaml.cs +++ b/samples/todoapp/TodoApp.MAUI/MainPage.xaml.cs @@ -29,7 +29,6 @@ public void OnListItemTapped(object sender, ItemTappedEventArgs e) { if (e.Item is TodoItem item) { - Debug.WriteLine($"[UI] >>> Item clicked: {item.Id}"); this._viewModel.SelectItemCommand.Execute(item); } diff --git a/samples/todoapp/TodoApp.WPF/App.xaml b/samples/todoapp/TodoApp.WPF/App.xaml index b4f48400..1acf29e3 100644 --- a/samples/todoapp/TodoApp.WPF/App.xaml +++ b/samples/todoapp/TodoApp.WPF/App.xaml @@ -1,9 +1,8 @@ - - diff --git a/samples/todoapp/TodoApp.WPF/App.xaml.cs b/samples/todoapp/TodoApp.WPF/App.xaml.cs index 3bec5789..9bf55a19 100644 --- a/samples/todoapp/TodoApp.WPF/App.xaml.cs +++ b/samples/todoapp/TodoApp.WPF/App.xaml.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using System.Windows; using TodoApp.WPF.Database; +using TodoApp.WPF.Services; using TodoApp.WPF.ViewModels; namespace TodoApp.WPF; @@ -17,15 +18,10 @@ namespace TodoApp.WPF; public partial class App : Application, IDisposable { private readonly SqliteConnection dbConnection; - - /// - /// The IoC service provider - /// public IServiceProvider Services { get; } public App() { - // Create the connection to the SQLite database this.dbConnection = new SqliteConnection("Data Source=:memory:"); this.dbConnection.Open(); @@ -33,6 +29,7 @@ public App() Services = new ServiceCollection() .AddTransient() .AddScoped() + .AddScoped() .AddDbContext(options => options.UseSqlite(this.dbConnection)) .BuildServiceProvider(); @@ -42,7 +39,6 @@ public App() private void InitializeDatabase() { - // using IServiceScope scope = Ioc.Default.CreateScope(); using IServiceScope scope = Services.CreateScope(); IDbInitializer initializer = scope.ServiceProvider.GetRequiredService(); initializer.Initialize(); diff --git a/samples/todoapp/TodoApp.WPF/Converters/BooleanToImageConverter.cs b/samples/todoapp/TodoApp.WPF/Converters/BooleanToImageConverter.cs new file mode 100644 index 00000000..3931eaf3 --- /dev/null +++ b/samples/todoapp/TodoApp.WPF/Converters/BooleanToImageConverter.cs @@ -0,0 +1,32 @@ +// 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 System.Globalization; +using System.Windows.Data; +using System.Windows.Media.Imaging; + +namespace TodoApp.WPF.Converters; + +/// +/// A converter to convert the boolean for IsComplete into one of two images. +/// +public class BooleanToImageConverter : IValueConverter +{ + private const string baseUri = "pack://application:,,,/Images"; + + public object? Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool booleanValue) + { + return new BitmapImage(new Uri(booleanValue ? $"{baseUri}/completed.png" : $"{baseUri}/incomplete.png")); + } + + return null; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/samples/todoapp/TodoApp.WPF/Database/AppDbContext.cs b/samples/todoapp/TodoApp.WPF/Database/AppDbContext.cs index dfd5c460..b00f3b1c 100644 --- a/samples/todoapp/TodoApp.WPF/Database/AppDbContext.cs +++ b/samples/todoapp/TodoApp.WPF/Database/AppDbContext.cs @@ -5,7 +5,6 @@ using CommunityToolkit.Datasync.Client.Http; using CommunityToolkit.Datasync.Client.Offline; using Microsoft.EntityFrameworkCore; -using TodoApp.WPF.Services; namespace TodoApp.WPF.Database; @@ -13,29 +12,29 @@ public class AppDbContext(DbContextOptions options) : DbContext(op { public DbSet TodoItems => Set(); - // protected override void OnDatasyncInitialization(DatasyncOfflineOptionsBuilder optionsBuilder) - // { - // HttpClientOptions clientOptions = new() - // { - // Endpoint = new Uri("https://Y.azurewebsites.net/"), - // HttpPipeline = [new LoggingHandler()] - // }; - // _ = optionsBuilder.UseHttpClientOptions(clientOptions); - // } + //protected override void OnDatasyncInitialization(DatasyncOfflineOptionsBuilder optionsBuilder) + //{ + // HttpClientOptions clientOptions = new() + // { + // Endpoint = new Uri("https://YOURSITEHERE.azurewebsites.net/"), + // HttpPipeline = [new LoggingHandler()] + // }; + // _ = optionsBuilder.UseHttpClientOptions(clientOptions); + //} public async Task SynchronizeAsync(CancellationToken cancellationToken = default) { - // PushResult pushResult = await this.PushAsync(cancellationToken); - // if (!pushResult.IsSuccessful) - // { - // throw new ApplicationException($"Push failed: {pushResult.FailedRequests.FirstOrDefault().Value.ReasonPhrase}"); - // } - - // PullResult pullResult = await this.PullAsync(cancellationToken); - // if (!pullResult.IsSuccessful) - // { - // throw new ApplicationException($"Pull failed: {pullResult.FailedRequests.FirstOrDefault().Value.ReasonPhrase}"); - // } + //PushResult pushResult = await this.PushAsync(cancellationToken); + //if (!pushResult.IsSuccessful) + //{ + // throw new ApplicationException($"Push failed: {pushResult.FailedRequests.FirstOrDefault().Value.ReasonPhrase}"); + //} + + //PullResult pullResult = await this.PullAsync(cancellationToken); + //if (!pullResult.IsSuccessful) + //{ + // throw new ApplicationException($"Pull failed: {pullResult.FailedRequests.FirstOrDefault().Value.ReasonPhrase}"); + //} } } @@ -49,7 +48,10 @@ public class DbContextInitializer(AppDbContext context) : IDbInitializer { /// public void Initialize() - => context.Database.EnsureCreated(); + { + _ = context.Database.EnsureCreated(); + // Task.Run(async () => await context.SynchronizeAsync()); + } /// public Task InitializeAsync(CancellationToken cancellationToken = default) diff --git a/samples/todoapp/TodoApp.WPF/Database/TodoItem.cs b/samples/todoapp/TodoApp.WPF/Database/TodoItem.cs index 1205316e..f46888d6 100644 --- a/samples/todoapp/TodoApp.WPF/Database/TodoItem.cs +++ b/samples/todoapp/TodoApp.WPF/Database/TodoItem.cs @@ -2,15 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Text.Json; - namespace TodoApp.WPF.Database; - public class TodoItem : OfflineClientEntity { public string Title { get; set; } = string.Empty; public bool IsComplete { get; set; } = false; - - public override string ToString() - => JsonSerializer.Serialize(this); -} \ No newline at end of file +} diff --git a/samples/todoapp/TodoApp.WPF/Images/AddItem.png b/samples/todoapp/TodoApp.WPF/Images/AddItem.png deleted file mode 100644 index 33664793..00000000 Binary files a/samples/todoapp/TodoApp.WPF/Images/AddItem.png and /dev/null differ diff --git a/samples/todoapp/TodoApp.WPF/Images/RefreshItems.png b/samples/todoapp/TodoApp.WPF/Images/RefreshItems.png deleted file mode 100644 index 51748edd..00000000 Binary files a/samples/todoapp/TodoApp.WPF/Images/RefreshItems.png and /dev/null differ diff --git a/samples/todoapp/TodoApp.WPF/Images/completed.png b/samples/todoapp/TodoApp.WPF/Images/completed.png new file mode 100644 index 00000000..8ac0e7f1 Binary files /dev/null and b/samples/todoapp/TodoApp.WPF/Images/completed.png differ diff --git a/samples/todoapp/TodoApp.WPF/Images/incomplete.png b/samples/todoapp/TodoApp.WPF/Images/incomplete.png new file mode 100644 index 00000000..a715fd31 Binary files /dev/null and b/samples/todoapp/TodoApp.WPF/Images/incomplete.png differ diff --git a/samples/todoapp/TodoApp.WPF/MainWindow.xaml b/samples/todoapp/TodoApp.WPF/MainWindow.xaml index cfcdeca3..4e006553 100644 --- a/samples/todoapp/TodoApp.WPF/MainWindow.xaml +++ b/samples/todoapp/TodoApp.WPF/MainWindow.xaml @@ -4,82 +4,66 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:TodoApp.WPF" + xmlns:conv="clr-namespace:TodoApp.WPF.Converters" mc:Ignorable="d" - Title="TodoApp" Height="600" Width="450"> + Title="MainWindow" Height="420" Width="800" + ResizeMode="CanMinimize"> - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - + + + + +