diff --git a/docs/content/samples/todoapp/assets/TodoApp.Avalonia.Desktop.png b/docs/content/samples/todoapp/assets/TodoApp.Avalonia.Desktop.png new file mode 100644 index 00000000..1e281c0d Binary files /dev/null and b/docs/content/samples/todoapp/assets/TodoApp.Avalonia.Desktop.png differ diff --git a/docs/content/samples/todoapp/avalonia.md b/docs/content/samples/todoapp/avalonia.md new file mode 100644 index 00000000..a8c1e812 --- /dev/null +++ b/docs/content/samples/todoapp/avalonia.md @@ -0,0 +1,80 @@ ++++ +title = "Avalonia" ++++ + +## Run the application first + +The Avalonia sample uses an in-memory Sqlite store for storing its data. The sample can run on Desktop, Mobile and Browser. To run the application locally: + +* [Configure Visual Studio for Avalonia development](https://docs.avaloniaui.net/docs/welcome). +* Open `samples/todoapp/Samples.TodoApp.sln` in Visual Studio. +* In the Solution Explorer, expand the folder `TodoApp.Avalonia` and right-click the `TodoApp.Avalonia.Desktop` project, then select **Set as Startup Project**. +* Select a target (in the top bar), then press F5 to run the application. + +> [!TIP] +> We suggest to start testing and debugging using the Desktop App first. Once you feel confident, you can also try out Mobile or Browser version. + +> [!NOTE] +> If you bump into issues at this point, please visit [Avalonia.Docs](https://docs.avaloniaui.net) and [Avalonia.Samples](https://github.com/AvaloniaUI/Avalonia.Samples) for some basic getting-started tutorials. + +This is how the sample will look like: +![Avalonia sample on Desktop](assets/TodoApp.Avalonia.Desktop.png) + + +## 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`: + + ```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}"); + } + } + ``` + +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. + +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 1204e402..44851fab 100644 --- a/samples/todoapp/Samples.TodoApp.sln +++ b/samples/todoapp/Samples.TodoApp.sln @@ -15,6 +15,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApp.MAUI", "TodoApp.MAU EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoApp.WPF", "TodoApp.WPF\TodoApp.WPF.csproj", "{A0996FB8-890D-4E90-A881-01F9EF709711}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TodoApp.Avalonia", "TodoApp.Avalonia", "{9A8B7D7F-1AF1-4C1C-A74A-E422BB680C6E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoApp.Avalonia", "TodoApp.Avalonia\TodoApp.Avalonia\TodoApp.Avalonia.csproj", "{539C6E0F-8F23-4AE0-B8E6-7E72C53B890A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoApp.Avalonia.Android", "TodoApp.Avalonia\TodoApp.Avalonia.Android\TodoApp.Avalonia.Android.csproj", "{9C2BA2A4-4AD6-4B67-BB6B-29A9024C33C4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoApp.Avalonia.Desktop", "TodoApp.Avalonia\TodoApp.Avalonia.Desktop\TodoApp.Avalonia.Desktop.csproj", "{3D741850-6FAA-4F36-BD58-F6ECE0CE55D7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoApp.Avalonia.iOS", "TodoApp.Avalonia\TodoApp.Avalonia.iOS\TodoApp.Avalonia.iOS.csproj", "{DFCE2057-9F7B-4E1A-9C83-B524D9A82FF1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoApp.Avalonia.Browser", "TodoApp.Avalonia\TodoApp.Avalonia.Browser\TodoApp.Avalonia.Browser.csproj", "{E8BB1310-477D-44B0-B13E-77F09433D0A3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -123,12 +135,97 @@ Global {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 + {539C6E0F-8F23-4AE0-B8E6-7E72C53B890A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {539C6E0F-8F23-4AE0-B8E6-7E72C53B890A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {539C6E0F-8F23-4AE0-B8E6-7E72C53B890A}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {539C6E0F-8F23-4AE0-B8E6-7E72C53B890A}.Debug|ARM64.Build.0 = Debug|Any CPU + {539C6E0F-8F23-4AE0-B8E6-7E72C53B890A}.Debug|x64.ActiveCfg = Debug|Any CPU + {539C6E0F-8F23-4AE0-B8E6-7E72C53B890A}.Debug|x64.Build.0 = Debug|Any CPU + {539C6E0F-8F23-4AE0-B8E6-7E72C53B890A}.Debug|x86.ActiveCfg = Debug|Any CPU + {539C6E0F-8F23-4AE0-B8E6-7E72C53B890A}.Debug|x86.Build.0 = Debug|Any CPU + {539C6E0F-8F23-4AE0-B8E6-7E72C53B890A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {539C6E0F-8F23-4AE0-B8E6-7E72C53B890A}.Release|Any CPU.Build.0 = Release|Any CPU + {539C6E0F-8F23-4AE0-B8E6-7E72C53B890A}.Release|ARM64.ActiveCfg = Release|Any CPU + {539C6E0F-8F23-4AE0-B8E6-7E72C53B890A}.Release|ARM64.Build.0 = Release|Any CPU + {539C6E0F-8F23-4AE0-B8E6-7E72C53B890A}.Release|x64.ActiveCfg = Release|Any CPU + {539C6E0F-8F23-4AE0-B8E6-7E72C53B890A}.Release|x64.Build.0 = Release|Any CPU + {539C6E0F-8F23-4AE0-B8E6-7E72C53B890A}.Release|x86.ActiveCfg = Release|Any CPU + {539C6E0F-8F23-4AE0-B8E6-7E72C53B890A}.Release|x86.Build.0 = Release|Any CPU + {9C2BA2A4-4AD6-4B67-BB6B-29A9024C33C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C2BA2A4-4AD6-4B67-BB6B-29A9024C33C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C2BA2A4-4AD6-4B67-BB6B-29A9024C33C4}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {9C2BA2A4-4AD6-4B67-BB6B-29A9024C33C4}.Debug|ARM64.Build.0 = Debug|Any CPU + {9C2BA2A4-4AD6-4B67-BB6B-29A9024C33C4}.Debug|x64.ActiveCfg = Debug|Any CPU + {9C2BA2A4-4AD6-4B67-BB6B-29A9024C33C4}.Debug|x64.Build.0 = Debug|Any CPU + {9C2BA2A4-4AD6-4B67-BB6B-29A9024C33C4}.Debug|x86.ActiveCfg = Debug|Any CPU + {9C2BA2A4-4AD6-4B67-BB6B-29A9024C33C4}.Debug|x86.Build.0 = Debug|Any CPU + {9C2BA2A4-4AD6-4B67-BB6B-29A9024C33C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C2BA2A4-4AD6-4B67-BB6B-29A9024C33C4}.Release|Any CPU.Build.0 = Release|Any CPU + {9C2BA2A4-4AD6-4B67-BB6B-29A9024C33C4}.Release|ARM64.ActiveCfg = Release|Any CPU + {9C2BA2A4-4AD6-4B67-BB6B-29A9024C33C4}.Release|ARM64.Build.0 = Release|Any CPU + {9C2BA2A4-4AD6-4B67-BB6B-29A9024C33C4}.Release|x64.ActiveCfg = Release|Any CPU + {9C2BA2A4-4AD6-4B67-BB6B-29A9024C33C4}.Release|x64.Build.0 = Release|Any CPU + {9C2BA2A4-4AD6-4B67-BB6B-29A9024C33C4}.Release|x86.ActiveCfg = Release|Any CPU + {9C2BA2A4-4AD6-4B67-BB6B-29A9024C33C4}.Release|x86.Build.0 = Release|Any CPU + {3D741850-6FAA-4F36-BD58-F6ECE0CE55D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D741850-6FAA-4F36-BD58-F6ECE0CE55D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D741850-6FAA-4F36-BD58-F6ECE0CE55D7}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {3D741850-6FAA-4F36-BD58-F6ECE0CE55D7}.Debug|ARM64.Build.0 = Debug|Any CPU + {3D741850-6FAA-4F36-BD58-F6ECE0CE55D7}.Debug|x64.ActiveCfg = Debug|Any CPU + {3D741850-6FAA-4F36-BD58-F6ECE0CE55D7}.Debug|x64.Build.0 = Debug|Any CPU + {3D741850-6FAA-4F36-BD58-F6ECE0CE55D7}.Debug|x86.ActiveCfg = Debug|Any CPU + {3D741850-6FAA-4F36-BD58-F6ECE0CE55D7}.Debug|x86.Build.0 = Debug|Any CPU + {3D741850-6FAA-4F36-BD58-F6ECE0CE55D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D741850-6FAA-4F36-BD58-F6ECE0CE55D7}.Release|Any CPU.Build.0 = Release|Any CPU + {3D741850-6FAA-4F36-BD58-F6ECE0CE55D7}.Release|ARM64.ActiveCfg = Release|Any CPU + {3D741850-6FAA-4F36-BD58-F6ECE0CE55D7}.Release|ARM64.Build.0 = Release|Any CPU + {3D741850-6FAA-4F36-BD58-F6ECE0CE55D7}.Release|x64.ActiveCfg = Release|Any CPU + {3D741850-6FAA-4F36-BD58-F6ECE0CE55D7}.Release|x64.Build.0 = Release|Any CPU + {3D741850-6FAA-4F36-BD58-F6ECE0CE55D7}.Release|x86.ActiveCfg = Release|Any CPU + {3D741850-6FAA-4F36-BD58-F6ECE0CE55D7}.Release|x86.Build.0 = Release|Any CPU + {DFCE2057-9F7B-4E1A-9C83-B524D9A82FF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DFCE2057-9F7B-4E1A-9C83-B524D9A82FF1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DFCE2057-9F7B-4E1A-9C83-B524D9A82FF1}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {DFCE2057-9F7B-4E1A-9C83-B524D9A82FF1}.Debug|ARM64.Build.0 = Debug|Any CPU + {DFCE2057-9F7B-4E1A-9C83-B524D9A82FF1}.Debug|x64.ActiveCfg = Debug|Any CPU + {DFCE2057-9F7B-4E1A-9C83-B524D9A82FF1}.Debug|x64.Build.0 = Debug|Any CPU + {DFCE2057-9F7B-4E1A-9C83-B524D9A82FF1}.Debug|x86.ActiveCfg = Debug|Any CPU + {DFCE2057-9F7B-4E1A-9C83-B524D9A82FF1}.Debug|x86.Build.0 = Debug|Any CPU + {DFCE2057-9F7B-4E1A-9C83-B524D9A82FF1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DFCE2057-9F7B-4E1A-9C83-B524D9A82FF1}.Release|Any CPU.Build.0 = Release|Any CPU + {DFCE2057-9F7B-4E1A-9C83-B524D9A82FF1}.Release|ARM64.ActiveCfg = Release|Any CPU + {DFCE2057-9F7B-4E1A-9C83-B524D9A82FF1}.Release|ARM64.Build.0 = Release|Any CPU + {DFCE2057-9F7B-4E1A-9C83-B524D9A82FF1}.Release|x64.ActiveCfg = Release|Any CPU + {DFCE2057-9F7B-4E1A-9C83-B524D9A82FF1}.Release|x64.Build.0 = Release|Any CPU + {DFCE2057-9F7B-4E1A-9C83-B524D9A82FF1}.Release|x86.ActiveCfg = Release|Any CPU + {DFCE2057-9F7B-4E1A-9C83-B524D9A82FF1}.Release|x86.Build.0 = Release|Any CPU + {E8BB1310-477D-44B0-B13E-77F09433D0A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E8BB1310-477D-44B0-B13E-77F09433D0A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E8BB1310-477D-44B0-B13E-77F09433D0A3}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {E8BB1310-477D-44B0-B13E-77F09433D0A3}.Debug|ARM64.Build.0 = Debug|Any CPU + {E8BB1310-477D-44B0-B13E-77F09433D0A3}.Debug|x64.ActiveCfg = Debug|Any CPU + {E8BB1310-477D-44B0-B13E-77F09433D0A3}.Debug|x64.Build.0 = Debug|Any CPU + {E8BB1310-477D-44B0-B13E-77F09433D0A3}.Debug|x86.ActiveCfg = Debug|Any CPU + {E8BB1310-477D-44B0-B13E-77F09433D0A3}.Debug|x86.Build.0 = Debug|Any CPU + {E8BB1310-477D-44B0-B13E-77F09433D0A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E8BB1310-477D-44B0-B13E-77F09433D0A3}.Release|Any CPU.Build.0 = Release|Any CPU + {E8BB1310-477D-44B0-B13E-77F09433D0A3}.Release|ARM64.ActiveCfg = Release|Any CPU + {E8BB1310-477D-44B0-B13E-77F09433D0A3}.Release|ARM64.Build.0 = Release|Any CPU + {E8BB1310-477D-44B0-B13E-77F09433D0A3}.Release|x64.ActiveCfg = Release|Any CPU + {E8BB1310-477D-44B0-B13E-77F09433D0A3}.Release|x64.Build.0 = Release|Any CPU + {E8BB1310-477D-44B0-B13E-77F09433D0A3}.Release|x86.ActiveCfg = Release|Any CPU + {E8BB1310-477D-44B0-B13E-77F09433D0A3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {2AC73FBE-9E76-4702-B551-B5884383CC68} = {7183ECEC-9F44-48CE-BB97-AA2445170D5E} + {539C6E0F-8F23-4AE0-B8E6-7E72C53B890A} = {9A8B7D7F-1AF1-4C1C-A74A-E422BB680C6E} + {9C2BA2A4-4AD6-4B67-BB6B-29A9024C33C4} = {9A8B7D7F-1AF1-4C1C-A74A-E422BB680C6E} + {3D741850-6FAA-4F36-BD58-F6ECE0CE55D7} = {9A8B7D7F-1AF1-4C1C-A74A-E422BB680C6E} + {DFCE2057-9F7B-4E1A-9C83-B524D9A82FF1} = {9A8B7D7F-1AF1-4C1C-A74A-E422BB680C6E} + {E8BB1310-477D-44B0-B13E-77F09433D0A3} = {9A8B7D7F-1AF1-4C1C-A74A-E422BB680C6E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {91B9DE2A-8B79-4DC4-8235-216CD07F1CB2} diff --git a/samples/todoapp/TodoApp.Avalonia/.gitignore b/samples/todoapp/TodoApp.Avalonia/.gitignore new file mode 100644 index 00000000..8afdcb63 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/.gitignore @@ -0,0 +1,454 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# JetBrains Rider +.idea/ +*.sln.iml + +## +## Visual Studio Code +## +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json diff --git a/samples/todoapp/TodoApp.Avalonia/Directory.Build.props b/samples/todoapp/TodoApp.Avalonia/Directory.Build.props new file mode 100644 index 00000000..89dc4436 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/Directory.Build.props @@ -0,0 +1,6 @@ + + + enable + 11.1.0 + + diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Icon.png b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Icon.png new file mode 100644 index 00000000..41a2a618 Binary files /dev/null and b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Icon.png differ diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/MainActivity.cs b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/MainActivity.cs new file mode 100644 index 00000000..5ad26437 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/MainActivity.cs @@ -0,0 +1,21 @@ +using Android.App; +using Android.Content.PM; +using Avalonia; +using Avalonia.Android; + +namespace TodoApp.Avalonia.Android; + +[Activity( + Label = "TodoApp.Avalonia.Android", + Theme = "@style/MyTheme.NoActionBar", + Icon = "@drawable/icon", + MainLauncher = true, + ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] +public class MainActivity : AvaloniaMainActivity +{ + protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) + { + return base.CustomizeAppBuilder(builder) + .WithInterFont(); + } +} \ No newline at end of file diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Properties/AndroidManifest.xml b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Properties/AndroidManifest.xml new file mode 100644 index 00000000..7d86d689 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Properties/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/AboutResources.txt b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/AboutResources.txt new file mode 100644 index 00000000..c2bca974 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/AboutResources.txt @@ -0,0 +1,44 @@ +Images, layout descriptions, binary blobs and string dictionaries can be included +in your application as resource files. Various Android APIs are designed to +operate on the resource IDs instead of dealing with images, strings or binary blobs +directly. + +For example, a sample Android app that contains a user interface layout (main.axml), +an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) +would keep its resources in the "Resources" directory of the application: + +Resources/ + drawable/ + icon.png + + layout/ + main.axml + + values/ + strings.xml + +In order to get the build system to recognize Android resources, set the build action to +"AndroidResource". The native Android APIs do not operate directly with filenames, but +instead operate on resource IDs. When you compile an Android application that uses resources, +the build system will package the resources for distribution and generate a class called "R" +(this is an Android convention) that contains the tokens for each one of the resources +included. For example, for the above Resources layout, this is what the R class would expose: + +public class R { + public class drawable { + public const int icon = 0x123; + } + + public class layout { + public const int main = 0x456; + } + + public class strings { + public const int first_string = 0xabc; + public const int second_string = 0xbcd; + } +} + +You would then use R.drawable.icon to reference the drawable/icon.png file, or R.layout.main +to reference the layout/main.axml file, or R.strings.first_string to reference the first +string in the dictionary file values/strings.xml. \ No newline at end of file diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/drawable-night-v31/avalonia_anim.xml b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/drawable-night-v31/avalonia_anim.xml new file mode 100644 index 00000000..dde4b5a7 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/drawable-night-v31/avalonia_anim.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/drawable-v31/avalonia_anim.xml b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/drawable-v31/avalonia_anim.xml new file mode 100644 index 00000000..94f27d9e --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/drawable-v31/avalonia_anim.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/drawable/splash_screen.xml b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/drawable/splash_screen.xml new file mode 100644 index 00000000..2e920b4b --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/drawable/splash_screen.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/values-night/colors.xml b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/values-night/colors.xml new file mode 100644 index 00000000..3d47b6fc --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/values-night/colors.xml @@ -0,0 +1,4 @@ + + + #212121 + diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/values-v31/styles.xml b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/values-v31/styles.xml new file mode 100644 index 00000000..d5ecec43 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/values-v31/styles.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/values/colors.xml b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/values/colors.xml new file mode 100644 index 00000000..59279d5d --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/values/colors.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/values/styles.xml b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/values/styles.xml new file mode 100644 index 00000000..6e534de2 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/Resources/values/styles.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/TodoApp.Avalonia.Android.csproj b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/TodoApp.Avalonia.Android.csproj new file mode 100644 index 00000000..ebf47e8e --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Android/TodoApp.Avalonia.Android.csproj @@ -0,0 +1,28 @@ + + + Exe + net8.0-android + 21 + enable + com.CompanyName.TodoApp.Avalonia + 1 + 1.0 + apk + false + + + + + Resources\drawable\Icon.png + + + + + + + + + + + + diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/Program.cs b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/Program.cs new file mode 100644 index 00000000..996ad395 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/Program.cs @@ -0,0 +1,17 @@ +using System.Runtime.Versioning; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Browser; +using TodoApp.Avalonia; + +[assembly: SupportedOSPlatform("browser")] + +internal sealed partial class Program +{ + private static Task Main(string[] args) => BuildAvaloniaApp() + .WithInterFont() + .StartBrowserAppAsync("out"); + + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure(); +} \ No newline at end of file diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/Properties/AssemblyInfo.cs b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..f31aed8e --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/Properties/AssemblyInfo.cs @@ -0,0 +1 @@ +[assembly: System.Runtime.Versioning.SupportedOSPlatform("browser")] \ No newline at end of file diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/Properties/launchSettings.json b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/Properties/launchSettings.json new file mode 100644 index 00000000..19a2eadf --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "TodoApp.Avalonia.Browser": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:7169;http://localhost:5235", + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + } + } +} diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/TodoApp.Avalonia.Browser.csproj b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/TodoApp.Avalonia.Browser.csproj new file mode 100644 index 00000000..9879acbd --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/TodoApp.Avalonia.Browser.csproj @@ -0,0 +1,15 @@ + + + net8.0-browser + Exe + true + + + + + + + + + + diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/runtimeconfig.template.json b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/runtimeconfig.template.json new file mode 100644 index 00000000..b96a9432 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/runtimeconfig.template.json @@ -0,0 +1,10 @@ +{ + "wasmHostProperties": { + "perHostConfig": [ + { + "name": "browser", + "host": "browser" + } + ] + } +} \ No newline at end of file diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/wwwroot/app.css b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/wwwroot/app.css new file mode 100644 index 00000000..1d6f754a --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/wwwroot/app.css @@ -0,0 +1,58 @@ +/* HTML styles for the splash screen */ +.avalonia-splash { + position: absolute; + height: 100%; + width: 100%; + background: white; + font-family: 'Outfit', sans-serif; + justify-content: center; + align-items: center; + display: flex; + pointer-events: none; +} + +/* Light theme styles */ +@media (prefers-color-scheme: light) { + .avalonia-splash { + background: white; + } + + .avalonia-splash h2 { + color: #1b2a4e; + } + + .avalonia-splash a { + color: #0D6EFD; + } +} + +@media (prefers-color-scheme: dark) { + .avalonia-splash { + background: #1b2a4e; + } + + .avalonia-splash h2 { + color: white; + } + + .avalonia-splash a { + color: white; + } +} + +.avalonia-splash h2 { + font-weight: 400; + font-size: 1.5rem; +} + +.avalonia-splash a { + text-decoration: none; + font-size: 2.5rem; + display: block; +} + +.avalonia-splash.splash-close { + transition: opacity 200ms, display 200ms; + display: none; + opacity: 0; +} diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/wwwroot/favicon.ico b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/wwwroot/favicon.ico new file mode 100644 index 00000000..da8d49ff Binary files /dev/null and b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/wwwroot/favicon.ico differ diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/wwwroot/index.html b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/wwwroot/index.html new file mode 100644 index 00000000..e98cc939 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/wwwroot/index.html @@ -0,0 +1,36 @@ + + + + + TodoApp.Avalonia.Browser + + + + + + +
+
+

+ Powered by + + + + + + + + + + + + + + +

+
+
+ + + + diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/wwwroot/main.js b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/wwwroot/main.js new file mode 100644 index 00000000..bf1555e4 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Browser/wwwroot/main.js @@ -0,0 +1,13 @@ +import { dotnet } from './_framework/dotnet.js' + +const is_browser = typeof window != "undefined"; +if (!is_browser) throw new Error(`Expected to be running in a browser`); + +const dotnetRuntime = await dotnet + .withDiagnosticTracing(false) + .withApplicationArgumentsFromQuery() + .create(); + +const config = dotnetRuntime.getConfig(); + +await dotnetRuntime.runMain(config.mainAssemblyName, [globalThis.location.href]); diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Desktop/Program.cs b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Desktop/Program.cs new file mode 100644 index 00000000..49d2ca0d --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Desktop/Program.cs @@ -0,0 +1,21 @@ +using System; +using Avalonia; + +namespace TodoApp.Avalonia.Desktop; + +sealed class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); +} \ No newline at end of file diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Desktop/TodoApp.Avalonia.Desktop.csproj b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Desktop/TodoApp.Avalonia.Desktop.csproj new file mode 100644 index 00000000..361d6bce --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Desktop/TodoApp.Avalonia.Desktop.csproj @@ -0,0 +1,24 @@ + + + WinExe + + net8.0 + enable + true + + + + app.manifest + + + + + + + + + + + + diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Desktop/app.manifest b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Desktop/app.manifest new file mode 100644 index 00000000..c387cccf --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.Desktop/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.iOS/AppDelegate.cs b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.iOS/AppDelegate.cs new file mode 100644 index 00000000..3d656678 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.iOS/AppDelegate.cs @@ -0,0 +1,23 @@ +using Foundation; +using UIKit; +using Avalonia; +using Avalonia.Controls; +using Avalonia.iOS; +using Avalonia.Media; + +namespace TodoApp.Avalonia.iOS; + +// The UIApplicationDelegate for the application. This class is responsible for launching the +// User Interface of the application, as well as listening (and optionally responding) to +// application events from iOS. +[Register("AppDelegate")] +#pragma warning disable CA1711 // Identifiers should not have incorrect suffix +public partial class AppDelegate : AvaloniaAppDelegate +#pragma warning restore CA1711 // Identifiers should not have incorrect suffix +{ + protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) + { + return base.CustomizeAppBuilder(builder) + .WithInterFont(); + } +} \ No newline at end of file diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.iOS/Entitlements.plist b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.iOS/Entitlements.plist new file mode 100644 index 00000000..0c67376e --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.iOS/Entitlements.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.iOS/Info.plist b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.iOS/Info.plist new file mode 100644 index 00000000..8446c28e --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.iOS/Info.plist @@ -0,0 +1,43 @@ + + + + + CFBundleDisplayName + TodoApp.Avalonia + CFBundleIdentifier + companyName.TodoApp.Avalonia + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + MinimumOSVersion + 13.0 + UIDeviceFamily + + 1 + 2 + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.iOS/Main.cs b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.iOS/Main.cs new file mode 100644 index 00000000..2395e797 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.iOS/Main.cs @@ -0,0 +1,14 @@ +using UIKit; + +namespace TodoApp.Avalonia.iOS; + +public class Application +{ + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} \ No newline at end of file diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.iOS/Resources/LaunchScreen.xib b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.iOS/Resources/LaunchScreen.xib new file mode 100644 index 00000000..fb678d64 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.iOS/Resources/LaunchScreen.xib @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.iOS/TodoApp.Avalonia.iOS.csproj b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.iOS/TodoApp.Avalonia.iOS.csproj new file mode 100644 index 00000000..abf3b43c --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.iOS/TodoApp.Avalonia.iOS.csproj @@ -0,0 +1,16 @@ + + + Exe + net8.0-ios + 13.0 + enable + + + + + + + + + + diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.sln b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.sln new file mode 100644 index 00000000..7499ac80 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia.sln @@ -0,0 +1,54 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32811.315 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApp.Avalonia", "TodoApp.Avalonia\TodoApp.Avalonia.csproj", "{EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApp.Avalonia.Desktop", "TodoApp.Avalonia.Desktop\TodoApp.Avalonia.Desktop.csproj", "{ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApp.Avalonia.Browser", "TodoApp.Avalonia.Browser\TodoApp.Avalonia.Browser.csproj", "{1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApp.Avalonia.iOS", "TodoApp.Avalonia.iOS\TodoApp.Avalonia.iOS.csproj", "{EBD9022F-BC83-4846-9A11-6F7F3772DC64}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApp.Avalonia.Android", "TodoApp.Avalonia.Android\TodoApp.Avalonia.Android.csproj", "{7AD1DAC8-7FBE-49D5-8614-7321233DB82E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3DA99C4E-89E3-4049-9C22-0A7EC60D83D8}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Release|Any CPU.Build.0 = Release|Any CPU + {ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Release|Any CPU.Build.0 = Release|Any CPU + {1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Release|Any CPU.Build.0 = Release|Any CPU + {EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Release|Any CPU.Build.0 = Release|Any CPU + {7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {83CB65B8-011F-4ED7-BCD3-A6CFA935EF7E} + EndGlobalSection +EndGlobal diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/App.axaml b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/App.axaml new file mode 100644 index 00000000..058e67ce --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/App.axaml @@ -0,0 +1,44 @@ + + + + + + M 16.500001 0 C 15.52778 0 14.688382 0.35533588 14.101563 0.87695315 C 13.514743 1.3985704 13.165432 2.0458273 12.939454 2.6484376 C 12.757473 3.1337185 12.652358 3.6048008 12.589844 4.0000001 L 3.0000001 4.0000001 A 1 1 0 0 0 2.0000001 5.0000002 A 1 1 0 0 0 3.0000001 6.0000002 L 4.0253908 6.0000002 A 1 1 0 0 0 4.0078126 6.1250002 L 7.1171877 31.000001 L 25.882813 31.000001 L 28.992188 6.1250002 A 1 1 0 0 0 28.996095 6.0000002 L 30.000001 6.0000002 A 1 1 0 0 0 31.000001 5.0000002 A 1 1 0 0 0 30.000001 4.0000001 L 20.410157 4.0000001 C 20.347644 3.6048008 20.24253 3.1337185 20.060548 2.6484376 C 19.834569 2.0458273 19.485258 1.3985704 18.898438 0.87695315 C 18.31162 0.35533585 17.472221 -7.4014797e-17 16.500001 0 z M 16.500001 2.0000001 C 17.027777 2.0000001 17.313384 2.1446656 17.570313 2.3730469 C 17.827243 2.6014283 18.040434 2.9541747 18.189454 3.3515626 C 18.271334 3.5699097 18.330561 3.7916862 18.375001 4.0000001 L 14.625 4.0000001 C 14.66944 3.7916862 14.728668 3.5699097 14.810547 3.3515626 C 14.959568 2.9541747 15.172758 2.6014283 15.429688 2.3730469 C 15.686617 2.1446655 15.972223 2.0000001 16.500001 2.0000001 z M 6.0078127 6.0000002 L 26.992188 6.0000002 L 24.117188 29.000001 L 8.8828128 29.000001 L 6.0078127 6.0000002 z M 16.500001 7.9199221 A 1 1 0 0 0 15.5 8.9199222 L 15.5 27.402345 A 1 1 0 0 0 16.500001 28.402345 A 1 1 0 0 0 17.500001 27.402345 L 17.500001 8.9199222 A 1 1 0 0 0 16.500001 7.9199221 z M 9.8886722 8.0058596 A 1 1 0 0 0 9.0058597 9.1113284 L 11.00586 27.111329 A 1 1 0 0 0 12.111329 27.994141 A 1 1 0 0 0 12.994141 26.888673 L 10.994141 8.8886722 A 1 1 0 0 0 9.8886722 8.0058596 z M 23.111329 8.0058596 A 1 1 0 0 0 22.00586 8.8886722 L 20.00586 26.888673 A 1 1 0 0 0 20.888673 27.994141 A 1 1 0 0 0 21.994141 27.111329 L 23.994141 9.1113284 A 1 1 0 0 0 23.111329 8.0058596 z + M 57.070314 2.7402345 C 48.797905 2.7402345 42.070314 9.4678252 42.070314 17.740235 C 42.070314 26.012646 48.797905 32.740235 57.070314 32.740235 C 65.342725 32.740235 72.070315 26.012646 72.070315 17.740235 C 72.070315 9.4678252 65.342725 2.7402345 57.070314 2.7402345 z M 57.070314 4.7402345 C 64.261847 4.7402345 70.070315 10.548703 70.070315 17.740235 C 70.070315 24.931768 64.261847 30.740235 57.070314 30.740235 C 49.878782 30.740235 44.070314 24.931768 44.070314 17.740235 C 44.070314 10.548703 49.878782 4.7402345 57.070314 4.7402345 z M 63.972658 10.779297 A 1 1 0 0 0 63.285158 11.115235 L 54.007814 21.621094 L 50.865236 17.949219 A 1 1 0 0 0 49.45508 17.837891 A 1 1 0 0 0 49.345705 19.248047 L 53.982424 24.669923 L 64.785158 12.439454 A 1 1 0 0 0 64.697268 11.029297 A 1 1 0 0 0 63.972658 10.779297 z + M 187.34323 0 C 152.30526 0 118.28156 9.9889238 88.951587 28.887905 C 70.367606 40.861893 54.245043 56.032253 41.221056 73.593235 L 2.0808211 34.641314 L 2.0808211 148.00624 L 115.99102 148.00624 L 62.772216 95.042275 C 90.904188 54.899315 137.49628 29.999987 187.34323 29.999987 C 271.08614 29.999987 339.21693 98.129776 339.21693 181.87369 L 369.21692 181.87369 C 369.21692 81.587792 287.62912 0 187.34323 0 z M 14.531083 201.87524 C 14.531083 302.16114 96.118875 383.74894 196.40477 383.74894 C 231.44274 383.74894 265.46744 373.76001 294.79641 354.86103 C 313.38039 342.88604 329.50296 327.71575 342.52694 310.15476 L 381.66624 349.10669 L 381.66624 235.74176 L 267.75698 235.74176 L 320.97578 288.70666 C 292.84381 328.84862 246.25172 353.74895 196.40477 353.74895 C 112.66086 353.74895 44.53107 285.61916 44.53107 201.87524 L 14.531083 201.87524 z + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/App.axaml.cs b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/App.axaml.cs new file mode 100644 index 00000000..b3444e8d --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/App.axaml.cs @@ -0,0 +1,119 @@ +using System; +using System.IO; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Xaml; +using Avalonia.Threading; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using TodoApp.Avalonia.Database; +using TodoApp.Avalonia.Services; +using TodoApp.Avalonia.ViewModels; +using TodoApp.Avalonia.Views; + +namespace TodoApp.Avalonia; + +public partial class App : Application, IDisposable +{ + private readonly SqliteConnection dbConnection; + + /// + /// Gets an see to configure App-Services. + /// + public IServiceProvider Services { get; } + + public App() + { + // For the sample we use a SqLite-DB which is in memory only. Therefore, there is no data persistence available. + // Feel free to adjust the connection string as needed. + string connectionString = "Data Source=:memory:"; + + this.dbConnection = new SqliteConnection(connectionString); + + this.dbConnection.Open(); + + // Create the IoC Services provider. + Services = new ServiceCollection() + .AddSingleton() + .AddScoped() + .AddDbContext(options => options.UseSqlite(this.dbConnection)) + .BuildServiceProvider(); + } + + /// + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + /// + public override void OnFrameworkInitializationCompleted() + { + // Line below is needed to remove Avalonia data validation. + // Without this line you will get duplicate validations from both Avalonia and CT + BindingPlugins.DataValidators.RemoveAt(0); + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow { DataContext = GetRequiredService() }; + } + else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) + { + singleViewPlatform.MainView = new MainView { DataContext = GetRequiredService() }; + } + + // Initialize the database after DataContext was set + InitializeDatabase(); + + base.OnFrameworkInitializationCompleted(); + } + + /// + /// This method initializes the database. + /// + private async void InitializeDatabase() + { + using IServiceScope scope = Services.CreateScope(); + IDbInitializer initializer = scope.ServiceProvider.GetRequiredService(); + await initializer.InitializeAsync(); + } + + /// + /// A helper method for getting a service from the -collection. + /// + /// + /// You can see this in action in the class. + /// + /// The type of the service. + /// An instance of the service + public static TService GetRequiredService() where TService : notnull + => ((App)App.Current!).Services.GetRequiredService(); + + #region IDisposable + + private bool hasDisposed; + + protected virtual void Dispose(bool disposing) + { + if (!this.hasDisposed) + { + if (disposing) + { + this.dbConnection.Close(); + } + + this.hasDisposed = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion +} \ No newline at end of file diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Assets/avalonia-logo.ico b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Assets/avalonia-logo.ico new file mode 100644 index 00000000..da8d49ff Binary files /dev/null and b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Assets/avalonia-logo.ico differ diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Database/AppDbContext.cs b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Database/AppDbContext.cs new file mode 100644 index 00000000..afd1f389 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Database/AppDbContext.cs @@ -0,0 +1,64 @@ +// 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; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Datasync.Client.Http; +using CommunityToolkit.Datasync.Client.Offline; +using Microsoft.EntityFrameworkCore; +using TodoApp.Avalonia.Services; + +namespace TodoApp.Avalonia.Database; + +public class AppDbContext(DbContextOptions options) : DbContext(options) +// 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); + //} + + 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}"); + // } + } + + /// + /// Adds some sample data to the database + /// + internal async Task AddSampleDataAsync(CancellationToken cancellationToken = default) + { + // If there are already some items, don't add sample data + if (await TodoItems.AnyAsync(cancellationToken: cancellationToken)) + { + return; + } + + await TodoItems.AddRangeAsync( + new TodoItem() { Id = Guid.NewGuid().ToString("N"), Title = """Say "Hello" to DataSync and Avalonia""" , IsComplete = true }, + new TodoItem() { Id = Guid.NewGuid().ToString("N"), Title = "Learn DataSync", IsComplete = false }, + new TodoItem() { Id = Guid.NewGuid().ToString("N"), Title = "Learn Avalonia", IsComplete = false }); + + await SaveChangesAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Database/DbContextInitializer.cs b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Database/DbContextInitializer.cs new file mode 100644 index 00000000..0de66b0e --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Database/DbContextInitializer.cs @@ -0,0 +1,38 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Infrastructure; +using TodoApp.Avalonia.ViewModels; + +namespace TodoApp.Avalonia.Database; + +/// +/// Use this class to initialize the database. In this sample, we just create +/// the database using . However, you +/// may want to use migrations. +/// +/// The for the database. +public class DbContextInitializer(AppDbContext context) : IDbInitializer +{ + /// + public void Initialize() + { + _ = context.Database.EnsureCreated(); + Task.Run(async () => await context.SynchronizeAsync()); + } + + /// + public async Task InitializeAsync(CancellationToken cancellationToken = default) + { + await context.Database.EnsureCreatedAsync(cancellationToken); + + await context.AddSampleDataAsync(cancellationToken); + + // Make sure the ViewModel has fetched the latest changes and is up to date + TodoListViewModel todoListViewModel = App.GetRequiredService(); + await todoListViewModel.RefreshItemsAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Database/IDbInitializer.cs b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Database/IDbInitializer.cs new file mode 100644 index 00000000..d4492286 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Database/IDbInitializer.cs @@ -0,0 +1,26 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace TodoApp.Avalonia.Database; + +/// +/// An interface to initialize a database. +/// +public interface IDbInitializer +{ + /// + /// Synchronously initialize the database. + /// + void Initialize(); + + /// + /// Asynchronously initialize the database. + /// + /// A to observe. + /// A task that resolves when complete. + Task InitializeAsync(CancellationToken cancellationToken = default); +} diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Database/OfflineClientEntry.cs b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Database/OfflineClientEntry.cs new file mode 100644 index 00000000..05642b04 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Database/OfflineClientEntry.cs @@ -0,0 +1,39 @@ +// 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; +using System.ComponentModel.DataAnnotations; + +namespace TodoApp.Avalonia.Database; + +/// +/// An abstract class for working with offline entities. +/// +public abstract class OfflineClientEntity +{ + /// + /// Gets or sets the ID of this item. + /// + /// + /// The default is . + /// + + [Key] + public string Id { get; set; } = Guid.NewGuid().ToString("N"); + + /// + /// Gets or sets the last update time. + /// + public DateTimeOffset? UpdatedAt { get; set; } + + /// + /// Gets or sets the version info. + /// + public string? Version { get; set; } + + /// + /// Gets or sets whether the item was deleted. + /// + public bool Deleted { get; set; } +} \ No newline at end of file diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Database/ToDoItem.cs b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Database/ToDoItem.cs new file mode 100644 index 00000000..fa3204e0 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Database/ToDoItem.cs @@ -0,0 +1,18 @@ +// 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. + +namespace TodoApp.Avalonia.Database; + +public class TodoItem : OfflineClientEntity +{ + /// + /// Gets or sets the content. + /// + public string Title { get; set; } = string.Empty; + + /// + /// Gets or sets if the task was completed. + /// + public bool IsComplete { get; set; } = false; +} diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Services/DialogManager.cs b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Services/DialogManager.cs new file mode 100644 index 00000000..85469bd4 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Services/DialogManager.cs @@ -0,0 +1,150 @@ +// 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; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Notifications; + +namespace TodoApp.Avalonia.Services; + +/// +/// The DialogManager is used to connect a given (most likely your ViewModel) to +/// a . +/// +/// +/// See also: Avalonia.Samples → ViewInteraction-Sample +/// +public class DialogManager +{ + + /// + /// This is a dictionary which stores the mappings between the and + /// + private static readonly Dictionary RegistrationMapper = + new Dictionary(); + + static DialogManager() + { + RegisterProperty.Changed.AddClassHandler(RegisterChanged); + } + + /// + /// This property handles the registration of Views and ViewModel + /// + public static readonly AttachedProperty RegisterProperty = + AvaloniaProperty.RegisterAttached( + "Register"); + + /// + /// Accessor for Attached property . + /// + public static IDialogBus? GetRegister(AvaloniaObject element) + { + return element.GetValue(RegisterProperty); + } + + /// + /// Accessor for Attached property . + /// + public static void SetRegister(AvaloniaObject element, IDialogBus value) + { + element.SetValue(RegisterProperty, value); + } + + /// + /// Called when a new Visual is registered + /// + /// If no Visual was provided + private static void RegisterChanged(Visual sender, AvaloniaPropertyChangedEventArgs e) + { + if (sender is null) + { + throw new InvalidOperationException("The DialogManager can only be registered on a Visual"); + } + + // Unregister any old registered context + if (e.OldValue != null) + { + RegistrationMapper.Remove((IDialogBus)e.OldValue); + } + + // Register any new context + if (e.NewValue != null) + { + RegistrationMapper.Add((IDialogBus)e.NewValue, sender); + } + } + + /// + /// Gets the associated for a given context. Returns null, if none was registered + /// + /// The context to lookup + /// The registered Visual for the context or null if none was found + public static Visual? GetVisualForContext(IDialogBus context) + { + return RegistrationMapper.TryGetValue(context, out Visual? result) ? result : null; + } + + /// + /// Gets the parent for the given context. Returns null, if no TopLevel was found + /// + /// The context to lookup + /// The registered TopLevel for the context or null if none was found + public static TopLevel? GetTopLevelForContext(IDialogBus context) + { + return TopLevel.GetTopLevel(GetVisualForContext(context)); + } +} + +/// +/// A helper class to manage dialogs via extension methods. Add more on your own +/// +public static class DialogHelper +{ + /// + /// Displays a message to the user with an error-style. + /// + /// The context to find the mapped Visual + /// The message to display + public static void ShowErrorAlert(this IDialogBus context, string message) + { + ShowAlert(context, message, NotificationType.Error); + } + + /// + /// Displays a message to the user with an info-style. + /// + /// The context to find the mapped Visual + /// The message to display + public static void ShowInfoAlert(this IDialogBus context, string message) + { + ShowAlert(context, message, NotificationType.Information); + } + + /// + /// Displays a message to the user with an success-style. + /// + /// The context to find the mapped Visual + /// The message to display + public static void ShowSuccessAlert(this IDialogBus context, string message) + { + ShowAlert(context, message, NotificationType.Success); + } + + /// + /// Helper method to display messages + /// + /// The context to find the mapped Visual + /// The message to display + /// The to use + public static void ShowAlert(this IDialogBus context, string message, NotificationType type) + { + if (Design.IsDesignMode) return; + INotificationService view = (INotificationService)DialogManager.GetVisualForContext(context)!; + + view.NotificationManager.Show(message, type, TimeSpan.FromSeconds(3)); + } +} \ No newline at end of file diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Services/IDialogBus.cs b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Services/IDialogBus.cs new file mode 100644 index 00000000..1cd99c78 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Services/IDialogBus.cs @@ -0,0 +1,13 @@ +// 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. + +namespace TodoApp.Avalonia.Services; + +/// +/// This interface is used to indicate that a class can be used to communicate with the View +/// +public interface IDialogBus +{ + +} \ No newline at end of file diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Services/INotificationService.cs b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Services/INotificationService.cs new file mode 100644 index 00000000..f2df5473 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Services/INotificationService.cs @@ -0,0 +1,18 @@ +// 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 Avalonia.Controls.Notifications; + +namespace TodoApp.Avalonia.Services; + +/// +/// An interface which a View needs to implement in order to show Notifications sent from any +/// +public interface INotificationService +{ + /// + /// Gets the NotificationManager which is used to display a NotificationMessage + /// + public WindowNotificationManager NotificationManager { get; } +} \ No newline at end of file diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Services/LoggingHandler.cs b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Services/LoggingHandler.cs new file mode 100644 index 00000000..971ee140 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Services/LoggingHandler.cs @@ -0,0 +1,45 @@ +// 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.Diagnostics; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace TodoApp.Avalonia.Services; + +/// +/// A delegating handler that logs the request/response to stdout. +/// +public class LoggingHandler : DelegatingHandler +{ + public LoggingHandler() : base() + { + } + + public LoggingHandler(HttpMessageHandler innerHandler) : base(innerHandler) + { + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + Debug.WriteLine($"[HTTP] >>> {request.Method} {request.RequestUri}"); + await WriteContentAsync(request.Content, cancellationToken); + + HttpResponseMessage response = await base.SendAsync(request, cancellationToken); + + Debug.WriteLine($"[HTTP] <<< {response.StatusCode} {response.ReasonPhrase}"); + await WriteContentAsync(response.Content, cancellationToken); + + return response; + } + + private static async Task WriteContentAsync(HttpContent content, CancellationToken cancellationToken = default) + { + if (content != null) + { + Debug.WriteLine($"[HTTP] >>> {await content.ReadAsStringAsync(cancellationToken)}"); + } + } +} diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/TodoApp.Avalonia.csproj b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/TodoApp.Avalonia.csproj new file mode 100644 index 00000000..adfa69b2 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/TodoApp.Avalonia.csproj @@ -0,0 +1,23 @@ + + + net8.0 + enable + latest + true + + + + + + + + + + + + + + + + + diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/ViewModels/DesingTimeViewModels.cs b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/ViewModels/DesingTimeViewModels.cs new file mode 100644 index 00000000..a8226ff7 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/ViewModels/DesingTimeViewModels.cs @@ -0,0 +1,20 @@ +// 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; +using Avalonia; +using Microsoft.Extensions.DependencyInjection; + +namespace TodoApp.Avalonia.ViewModels; +#if DEBUG + +/// +/// Holds some sample ViewModels to be shown in the Designer. +/// +public static class DesignTimeViewModels +{ + public static TodoListViewModel TodoListDesignerViewModel => + (Application.Current as App)?.Services.GetRequiredService() ?? throw new InvalidOperationException(); +} +#endif diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/ViewModels/TodoItemViewModel.cs b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/ViewModels/TodoItemViewModel.cs new file mode 100644 index 00000000..446ef7ab --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/ViewModels/TodoItemViewModel.cs @@ -0,0 +1,151 @@ +// 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; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using TodoApp.Avalonia.Database; +using TodoApp.Avalonia.Services; + +namespace TodoApp.Avalonia.ViewModels; + +/// +/// A ViewModel used to represent a . +/// +/// the to send notifications to the user +/// the to use +public partial class TodoItemViewModel(IDialogBus dialogBus, AppDbContext context) : ViewModelBase +{ + // A reference to the provided TodoItem + private TodoItem? _todoItem; + + /// + /// Creates a new ToDoItemViewModel for the given . + /// + /// The item to load + /// The which is the parent + /// The to use + public TodoItemViewModel(TodoItem item, IDialogBus dialogBus, AppDbContext context) : this(dialogBus, context) + { + // Init the properties with the given values. + this._IsComplete = item.IsComplete; + this._title = item.Title; + + this._todoItem = item; + } + + // NOTE: This property is made without source generator. Uncomment the line below to use the source generator. + // [ObservableProperty] + private bool _IsComplete; + + /// + /// Gets or sets the checked status of the item. + /// + public bool IsComplete + { + get { return this._IsComplete; } + set + { + // Store the old value in order to undo the changes, if the save operation failed. + bool oldValue = this._IsComplete; + + if (SetProperty(ref this._IsComplete, value)) + { + // save the item in case we have an updated value + SaveIsChecked(value, oldValue); + } + } + } + + /// + /// Saves the new value to the database + /// + /// the new value + /// the old value + private async void SaveIsChecked(bool newValue, bool oldValue) + { + await SaveIsCheckedAsync(newValue, oldValue); + } + + // a counter that can be used to track save requests. + // Only used to demonstrate an exception after every third save operation. + int updateCounter = 0; + + /// + /// + /// + /// the new value + /// the old value + /// the to use + /// If the item wasn't saved correctly + /// If the item to update wasn't found in the database. + private async Task SaveIsCheckedAsync(bool newValue, bool oldValue, CancellationToken cancellationToken = default) + { + TodoItem? storedItem = null; + try + { + // lookup the stored item + storedItem = await context.TodoItems.FindAsync([GetToDoItem().Id], cancellationToken); + + // this is just to show how errors are handled. Feel free to comment it. + if (++this.updateCounter % 3 == 0) + { + await Task.Delay(500); + throw new IOException("Unable to save the item a third time. Please try again."); + } + + if (storedItem is not null) + { + // update the stored item + storedItem.IsComplete = newValue; + + // Store the updated item in the database + _ = context.TodoItems.Update(storedItem); + _ = await context.SaveChangesAsync(cancellationToken); + + // Show an info to the user + dialogBus.ShowInfoAlert("Saved changes successfully"); + } + else + { + // If no item was found, throw an exection + throw new NullReferenceException("Item not found"); + } + } + catch (Exception ex) + { + // Set the Property back to it's old value in case of any exception. + SetProperty(ref this._IsComplete, oldValue, nameof(IsComplete)); + if (storedItem is not null) storedItem.IsComplete = oldValue; + dialogBus.ShowErrorAlert(ex.Message); + } + } + + /// + /// Gets or sets the Title of the to-do item + /// + [ObservableProperty] private string? _title; + + /// + /// Gets a for this Item-ViewModel + /// + public TodoItem GetToDoItem() + { + if (this._todoItem is not null) + { + this._todoItem.Title = Title ?? string.Empty; + this._todoItem.IsComplete = IsComplete; + return this._todoItem; + } + + // if no todoItem model is found, return a new one + return new TodoItem() + { + IsComplete = this.IsComplete, + Title = this.Title ?? string.Empty + }; + } +} \ No newline at end of file diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/ViewModels/TodoListViewModel.cs b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/ViewModels/TodoListViewModel.cs new file mode 100644 index 00000000..2ff98da2 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/ViewModels/TodoListViewModel.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Datasync.Client; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Microsoft.EntityFrameworkCore; +using TodoApp.Avalonia.Database; +using TodoApp.Avalonia.Services; + +namespace TodoApp.Avalonia.ViewModels; + +/// +/// A ViewModel used to represent a list of s +/// +/// the to use +public partial class TodoListViewModel(AppDbContext context) : ViewModelBase, IDialogBus +{ + /// + /// Gets or sets if the data is currently being refreshed. + /// + [ObservableProperty] + private bool isRefreshing; + + /// + /// Gets or sets a collection of s + /// + [ObservableProperty] + private ConcurrentObservableCollection items = []; + + // -- Adding new Items -- + + /// + /// This command is used to add a new Item to the + /// + [RelayCommand (CanExecute = nameof(CanAddItem))] + private async Task AddItemAsync(CancellationToken cancellationToken) + { + try + { + // Create the new item + TodoItem addition = new() + { + Id = Guid.NewGuid().ToString("N"), + Title = NewItemContent ?? throw new InvalidOperationException("New item content may not be null or empty") + }; + + // Add the item to the database + _ = context.TodoItems.Add(addition); + _ = await context.SaveChangesAsync(cancellationToken); + + // Add the item to the end of the list + Items.Add(new TodoItemViewModel(addition, this, context)); + + // Clear the NewItemContent-property for next insertion. + NewItemContent = string.Empty; + } + catch (Exception ex) + { + this.ShowErrorAlert(ex.Message); + } + } + + /// + /// Gets or set the content for new Items to add. If this string is not empty, the AddItemCommand will be enabled automatically + /// + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(AddItemCommand))] // This attribute will invalidate the command each time this property changes + private string? _newItemContent; + + /// + /// Returns true if a new Item can be added. We require to have the NewItem some text. + /// + private bool CanAddItem() => !string.IsNullOrWhiteSpace(NewItemContent); + + // -- Removing Items -- + + /// + /// This command removes the given Item from the -list + /// + /// the item to remove + [RelayCommand] + private async Task RemoveItemAsync(TodoItemViewModel item) + { + // Remove the item from the database + _ = context.TodoItems.Remove(item.GetToDoItem()); + _ = await context.SaveChangesAsync(); + + // Remove the given item from the list + Items.Remove(item); + } + + + /// + /// This command is used to refresh the entire -list. + /// + [RelayCommand] + public async Task RefreshItemsAsync(CancellationToken cancellationToken = default) + { + try + { + // Synchronize with the remote service + await context.SynchronizeAsync(cancellationToken); + + // Retrieve the items from the service + List dbItems = await context.TodoItems.OrderBy(item => item.Id).ToListAsync(cancellationToken); + + // Replace the items in the collection + Items.Clear(); + _ = Items.AddRange(dbItems.Select(x => new TodoItemViewModel(x, this, context))); + + this.ShowSuccessAlert("All Items loaded"); + } + catch (Exception ex) + { + this.ShowErrorAlert(ex.Message); + } + } +} \ No newline at end of file diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/ViewModels/ViewModelBase.cs b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/ViewModels/ViewModelBase.cs new file mode 100644 index 00000000..c81cf45f --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/ViewModels/ViewModelBase.cs @@ -0,0 +1,7 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace TodoApp.Avalonia.ViewModels; + +public abstract class ViewModelBase : ObservableObject +{ +} \ No newline at end of file diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Views/MainView.axaml b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Views/MainView.axaml new file mode 100644 index 00000000..86ffb4ce --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Views/MainView.axaml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Views/MainView.axaml.cs b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Views/MainView.axaml.cs new file mode 100644 index 00000000..b9981524 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Views/MainView.axaml.cs @@ -0,0 +1,48 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Notifications; +using Avalonia.Controls.Platform; +using Avalonia.Interactivity; +using TodoApp.Avalonia.Services; + +namespace TodoApp.Avalonia.Views; + +public partial class MainView : UserControl, INotificationService +{ + public MainView() + { + InitializeComponent(); + } + + /// + public WindowNotificationManager NotificationManager => this.WindowNotificationManager; + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + // Configure the view to handle soft-keyboard appearance. This needs to happen after control is loaded. + // See: https://docs.avaloniaui.net/docs/concepts/services/input-pane + IInputPane? inputPane = TopLevel.GetTopLevel(this)?.InputPane; + + if (inputPane != null) + { + inputPane.StateChanged += InputPaneOnStateChanged; + } + } + + /// + /// This method is being called whenever an is opened or closed + /// + private void InputPaneOnStateChanged(object? sender, InputPaneStateEventArgs e) + { + // We need to add a Padding to the bottom where the sof InputPane will occupy our App. + // Note: There may be more things to consider, like different rotations and Keyboard positions. + double bottomPadding = e.NewState == InputPaneState.Open + ? Math.Max(Bounds.Height - e.EndRect.Top, 0) + : 0; + + this.Padding = new Thickness(0, 0, 0, bottomPadding); + } +} \ No newline at end of file diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Views/MainWindow.axaml b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Views/MainWindow.axaml new file mode 100644 index 00000000..c2edfce3 --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Views/MainWindow.axaml @@ -0,0 +1,14 @@ + + + diff --git a/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Views/MainWindow.axaml.cs b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Views/MainWindow.axaml.cs new file mode 100644 index 00000000..76f1487c --- /dev/null +++ b/samples/todoapp/TodoApp.Avalonia/TodoApp.Avalonia/Views/MainWindow.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace TodoApp.Avalonia.Views; + +public partial class MainWindow : Window +{ + public MainWindow() + { + InitializeComponent(); + } +} \ No newline at end of file