Skip to content

Commit caf922e

Browse files
EilonBret JohnsonlutzroedermattleibowBret Johnson
authored
Add .NET MAUI Blazor Hybrid frontend app (#255)
## Purpose Adds a .NET MAUI Blazor Hybrid frontend app that re-uses the same Razor Components used in the Blazor Web frontend app. This helps showcase how a .NET MAUI cross-platform app can share the same Azure OpenAI functionality as a web app, and run natively on Windows, macOS, iOS, and Android devices. Fixes #253 ## Does this introduce a breaking change? [ ] Yes [x] No ## Pull Request Type What kind of change does this Pull Request introduce? [ ] Bugfix [x] Feature [ ] Code style update (formatting, local variables) [x] Refactoring (no functional changes, no api changes) [ ] Documentation content changes [ ] Other... Please describe: ## How to Test 1. Ensure all existing repo pre-reqs are installed for .NET 8 and Visual Studio, including the .NET MAUI workload 1. Clone repo 1. Follow the existing steps in the repo to publish the app to Azure 1. Get the URL of the app that was published to Azure Container Apps (something like `https://MY_HOSTED_APP.example.azurecontainerapps.io/`) 1. Update the file `app/maui-blazor/MauiProgram.cs` to use that URL to set the `client.BaseAddress` value (there is a 'TODO' note in the file) 1. Set the .NET MAUI Blazor Hybrid app as the startup project 1. Run the app on your device of choice (Windows Desktop, iPhone/iPad, MacBook, Android phone/tablet, etc.) and you should see a UI that exactly matches the Blazor Web app, except that the app is running locally (API calls still go out to Azure as usual) 1. Profit ## Other Information This PR conceptually has a few areas of change: 1. Almost all of the Razor components in the `app/frontend` Blazor Web app were moved to a new shared `app/SharedWebComponents` Razor Class Library (RCL) 2. The Blazor Web app now references that shared code 3. There is a new .NET MAUI Blazor Hybrid project that is mostly just the default template code. Almost all of the brand new code in this PR is standard template code. This new project references the same shared RCL to reuse the exact same functionality and implementation 4. The few notable changes in the apps are: 1. The PDF viewer logic was put behind a service interface so that it could use a web-based viewer in the Blazor Web app, and in the future a native PDF viewer in the .NET MAUI app 2. A UI rendering bug was fixed in `app/SharedWebComponents/Pages/Chat.razor` so that it displays properly on narrow screens (this was a pre-existing issue in the web app, but was very noticable on narrow mobile devices) 3. Some try/catch statements were added in the speech API calls cc @BretJohnson @mattleibow @maddymontaquila @lutzroeder --------- Co-authored-by: Bret Johnson <[email protected]> Co-authored-by: Lutz Roeder <[email protected]> Co-authored-by: Matthew Leibowitz <[email protected]> Co-authored-by: Bret Johnson <[email protected]> Co-authored-by: David Pine <[email protected]>
1 parent 030964c commit caf922e

File tree

113 files changed

+1374
-146
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

113 files changed

+1374
-146
lines changed

.vscode/launch.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@
2222
},
2323
"envFile": "${input:dotEnvFilePath}"
2424
},
25+
{
26+
"name": "Frontend: .NET MAUI client",
27+
"type": "maui",
28+
"request": "launch",
29+
"preLaunchTask": "maui: Build"
30+
},
2531
{
2632
"name": "Backend: Minimal API",
2733
"type": "coreclr",
@@ -51,4 +57,4 @@
5157
]
5258
}
5359
]
54-
}
60+
}

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,18 @@ If you have existing resources in Azure that you wish to use, you can configure
245245

246246
Navigate to <http://localhost:7181>, and test out the app.
247247

248+
#### Running locally with the .NET MAUI client
249+
250+
This sample includes a .NET MAUI client, packaging the experience as an app that can run on a Windows/macOS desktop or on Android and iOS devices. The MAUI client here is implemented using Blazor hybrid, letting it share most code with the website frontend.
251+
252+
1. Open _app/app-maui.sln_ to open the solution that includes the MAUI client
253+
254+
1. Edit _app/maui-blazor/MauiProgram.cs_, updating `client.BaseAddress` with the URL for the backend.
255+
256+
If it's running in Azure, use the URL for the service backend from the steps above. If running locally, use <http://localhost:7181>.
257+
258+
1. Set **MauiBlazor** as the startup project and run the app
259+
248260
#### Sharing Environments
249261

250262
Run the following if you want to give someone else access to the deployed and existing environment.

app/Directory.Packages.props

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<Project>
22
<PropertyGroup>
33
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
4+
<MauiVersion>8.0.6</MauiVersion>
45
</PropertyGroup>
56
<ItemGroup>
67
<PackageVersion Include="Azure.AI.FormRecognizer" Version="4.1.0" />
@@ -16,12 +17,15 @@
1617
<PackageVersion Include="Blazor.SpeechRecognition.WebAssembly" Version="8.0.0" />
1718
<PackageVersion Include="Blazor.SpeechSynthesis.WebAssembly" Version="8.0.0" />
1819
<PackageVersion Include="bunit" Version="1.25.3" />
20+
<PackageVersion Include="CommunityToolkit.Maui" Version="7.0.1" />
1921
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
2022
<PackageVersion Include="Markdig" Version="0.33.0" />
2123
<PackageVersion Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.22.0-beta3" />
2224
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.0" />
2325
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.0" />
2426
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.0" />
27+
<PackageVersion Include="Microsoft.AspNetCore.Components.Web" Version="8.0.0" />
28+
<PackageVersion Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)" />
2529
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Storage" Version="6.2.0" />
2630
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.16.2" />
2731
<PackageVersion Include="Microsoft.Azure.Functions.Worker" Version="1.20.0" />
@@ -30,7 +34,10 @@
3034
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
3135
<PackageVersion Include="Microsoft.Extensions.FileSystemGlobbing" Version="8.0.0" />
3236
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
37+
<PackageVersion Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
3338
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
39+
<PackageVersion Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
40+
<PackageVersion Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
3441
<PackageVersion Include="Microsoft.ML" Version="3.0.0" />
3542
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
3643
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.3.0" />
File renamed without changes.

app/frontend/Components/Answer.razor renamed to app/SharedWebComponents/Components/Answer.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
? @Icons.Custom.FileFormats.FilePdf
2323
: null;
2424
<MudChip Variant="Variant.Text" Color="Color.Info"
25-
Icon="@icon" OnClick="@(_ => OnShowCitation(citation))">
25+
Icon="@icon" OnClick="@(_ => OnShowCitationAsync(citation))">
2626
@($"{citation.Number}. {citation.Name}")
2727
</MudChip>
2828
}

app/frontend/Components/Answer.razor.Parser.cs renamed to app/SharedWebComponents/Components/Answer.razor.Parser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
namespace ClientApp.Components;
3+
namespace SharedWebComponents.Components;
44

55
public sealed partial class Answer
66
{

app/frontend/Components/Answer.razor.cs renamed to app/SharedWebComponents/Components/Answer.razor.cs

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
namespace ClientApp.Components;
3+
namespace SharedWebComponents.Components;
44

55
public sealed partial class Answer
66
{
77
[Parameter, EditorRequired] public required ApproachResponse Retort { get; set; }
88
[Parameter, EditorRequired] public required EventCallback<string> FollowupQuestionClicked { get; set; }
99

10-
[Inject] public required IDialogService Dialog { get; set; }
10+
[Inject] public required IPdfViewer PdfViewer { get; set; }
1111

1212
private HtmlParsedAnswer? _parsedAnswer;
1313

@@ -26,21 +26,7 @@ private async Task OnAskFollowupAsync(string followupQuestion)
2626
await FollowupQuestionClicked.InvokeAsync(followupQuestion);
2727
}
2828
}
29-
30-
private void OnShowCitation(CitationDetails citation) => Dialog.Show<PdfViewerDialog>(
31-
$"📄 {citation.Name}",
32-
new DialogParameters
33-
{
34-
[nameof(PdfViewerDialog.FileName)] = citation.Name,
35-
[nameof(PdfViewerDialog.BaseUrl)] = citation.BaseUrl,
36-
},
37-
new DialogOptions
38-
{
39-
MaxWidth = MaxWidth.Large,
40-
FullWidth = true,
41-
CloseButton = true,
42-
CloseOnEscapeKey = true
43-
});
29+
private ValueTask OnShowCitationAsync(CitationDetails citation) => PdfViewer.ShowDocumentAsync(citation.Name, citation.BaseUrl);
4430

4531
private MarkupString RemoveLeadingAndTrailingLineBreaks(string input) => (MarkupString)HtmlLineBreakRegex().Replace(input, "");
4632

app/frontend/Components/AnswerError.razor.cs renamed to app/SharedWebComponents/Components/AnswerError.razor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
namespace ClientApp.Components;
3+
namespace SharedWebComponents.Components;
44

55
public sealed partial class AnswerError
66
{

app/frontend/Components/Examples.razor.cs renamed to app/SharedWebComponents/Components/Examples.razor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
namespace ClientApp.Components;
3+
namespace SharedWebComponents.Components;
44

55
public sealed partial class Examples
66
{

app/frontend/Components/SettingsPanel.razor.cs renamed to app/SharedWebComponents/Components/SettingsPanel.razor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
namespace ClientApp.Components;
3+
namespace SharedWebComponents.Components;
44

55
public sealed partial class SettingsPanel : IDisposable
66
{

app/frontend/Components/SupportingContent.razor.Parser.cs renamed to app/SharedWebComponents/Components/SupportingContent.razor.Parser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
namespace ClientApp.Components;
3+
namespace SharedWebComponents.Components;
44

55
public sealed partial class SupportingContent
66
{

app/frontend/Components/SupportingContent.razor.cs renamed to app/SharedWebComponents/Components/SupportingContent.razor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
namespace ClientApp.Components;
3+
namespace SharedWebComponents.Components;
44

55
public sealed partial class SupportingContent
66
{

app/frontend/Components/VoiceDialog.razor.cs renamed to app/SharedWebComponents/Components/VoiceDialog.razor.cs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
namespace ClientApp.Components;
3+
namespace SharedWebComponents.Components;
44

55
public sealed partial class VoiceDialog : IDisposable
66
{
@@ -21,7 +21,20 @@ protected override async Task OnInitializedAsync()
2121
_state = RequestVoiceState.RequestingVoices;
2222

2323
await GetVoicesAsync();
24-
SpeechSynthesis.OnVoicesChanged(() => GetVoicesAsync(true));
24+
25+
try
26+
{
27+
SpeechSynthesis.OnVoicesChanged(() => GetVoicesAsync(true));
28+
}
29+
catch
30+
{
31+
// TODO: Find a better way to do this
32+
// The Blazor.SpeechSynthesis.WebAssembly API supports listening to changes,
33+
// however the underlying code does not do this using a DI-friendly way.
34+
// The code assumes the concrete implementation for the ISpeechSynthesisService
35+
// service is the concrete Web Assembly type which is not valid.
36+
// There is no alternative API that MAUI apps can use.
37+
}
2538

2639
_voicePreferences = new VoicePreferences(LocalStorage);
2740

@@ -55,7 +68,22 @@ private void OnValueChanged(string selectedVoice) => _voicePreferences = _voiceP
5568

5669
private void OnCancel() => Dialog.Close(DialogResult.Ok(_voicePreferences));
5770

58-
public void Dispose() => SpeechSynthesis.UnsubscribeFromVoicesChanged();
71+
public void Dispose()
72+
{
73+
try
74+
{
75+
SpeechSynthesis.UnsubscribeFromVoicesChanged();
76+
}
77+
catch
78+
{
79+
// TODO: Find a better way to do this
80+
// The Blazor.SpeechSynthesis.WebAssembly API supports listening to changes,
81+
// however the underlying code does not do this using a DI-friendly way.
82+
// The code assumes the concrete implementation for the ISpeechSynthesisService
83+
// service is the concrete Web Assembly type which is not valid.
84+
// There is no alternative API that MAUI apps can use.
85+
}
86+
}
5987
}
6088

6189
internal enum RequestVoiceState

app/frontend/Components/VoiceTextInput.razor.cs renamed to app/SharedWebComponents/Components/VoiceTextInput.razor.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
namespace ClientApp.Components;
3+
namespace SharedWebComponents.Components;
44

55
public sealed partial class VoiceTextInput : IDisposable
66
{
@@ -103,7 +103,7 @@ private void OnRecognized(string transcript)
103103
Value = Value switch
104104
{
105105
null => transcript,
106-
_ => $"{Value.Trim()} {transcript}".Trim()
106+
_ => $"{Value.Trim()} {transcript.Trim()}"
107107
};
108108

109109
StateHasChanged();

app/frontend/Extensions/LongExtensions.cs renamed to app/SharedWebComponents/Extensions/LongExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
namespace ClientApp.Extensions;
3+
namespace SharedWebComponents.Extensions;
44

55
public static class LongExtensions
66
{
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
global using System.Globalization;
4+
global using System.Net.Http.Json;
5+
global using System.Runtime.CompilerServices;
6+
global using System.Text;
7+
global using System.Text.Json;
8+
global using System.Text.Json.Serialization;
9+
global using System.Text.RegularExpressions;
10+
global using SharedWebComponents.Components;
11+
global using SharedWebComponents.Extensions;
12+
global using SharedWebComponents.Models;
13+
global using SharedWebComponents.Services;
14+
global using Markdig;
15+
global using Microsoft.AspNetCore.Components;
16+
global using Microsoft.AspNetCore.Components.Forms;
17+
global using Microsoft.AspNetCore.Components.Routing;
18+
global using Microsoft.AspNetCore.Components.Web;
19+
global using Microsoft.Extensions.DependencyInjection;
20+
global using Microsoft.Extensions.Logging;
21+
global using Microsoft.JSInterop;
22+
global using MudBlazor;
23+
global using Shared.Json;
24+
global using Shared.Models;
25+
26+
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ClientApp.Tests")]

app/frontend/Models/AnswerResult.cs renamed to app/SharedWebComponents/Models/AnswerResult.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
namespace ClientApp.Models;
3+
namespace SharedWebComponents.Models;
44

55
public readonly record struct AnswerResult<TRequest>(
66
bool IsSuccessful,

app/frontend/Models/AzureCulture.cs renamed to app/SharedWebComponents/Models/AzureCulture.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
namespace ClientApp.Models;
3+
namespace SharedWebComponents.Models;
44

55
public record class AzureCulture
66
{
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
namespace ClientApp.Models;
3+
namespace SharedWebComponents.Models;
44

55
public record CitationDetails(string Name, string BaseUrl, int Number = 0);

app/frontend/Models/LanguageDirection.cs renamed to app/SharedWebComponents/Models/LanguageDirection.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
namespace ClientApp.Models;
3+
namespace SharedWebComponents.Models;
44

55
public enum LanguageDirection
66
{

app/frontend/Models/RequestSettingsOverrides.cs renamed to app/SharedWebComponents/Models/RequestSettingsOverrides.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
namespace ClientApp.Models;
3+
namespace SharedWebComponents.Models;
44

55
public record RequestSettingsOverrides
66
{

app/frontend/Models/SharedCultures.cs renamed to app/SharedWebComponents/Models/SharedCultures.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
namespace ClientApp.Models;
3+
namespace SharedWebComponents.Models;
44

55
public record class SharedCultures
66
{

app/frontend/Models/UserQuestion.cs renamed to app/SharedWebComponents/Models/UserQuestion.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
namespace ClientApp.Models;
3+
namespace SharedWebComponents.Models;
44

55
public readonly record struct UserQuestion(
66
string Question,

app/frontend/Models/VoicePreferences.cs renamed to app/SharedWebComponents/Models/VoicePreferences.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
namespace ClientApp.Models;
3+
namespace SharedWebComponents.Models;
44

55
public record class VoicePreferences
66
{

app/frontend/Pages/Chat.razor renamed to app/SharedWebComponents/Pages/Chat.razor

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@
5050
else
5151
{
5252
<MudBadge Origin="Origin.TopLeft" Overlap="true" Color="Color.Secondary"
53-
Icon="@Icons.Material.Filled.AutoAwesome">
53+
Icon="@Icons.Material.Filled.AutoAwesome"
54+
Style="display:inherit">
5455
<Answer Retort="@answer" FollowupQuestionClicked="@OnAskQuestionAsync" />
5556
</MudBadge>
5657
}

app/frontend/Pages/Chat.razor.cs renamed to app/SharedWebComponents/Pages/Chat.razor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
namespace ClientApp.Pages;
3+
namespace SharedWebComponents.Pages;
44

55
public sealed partial class Chat
66
{

app/frontend/Pages/Docs.razor renamed to app/SharedWebComponents/Pages/Docs.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@
145145
<RowTemplate>
146146
<MudTd DataLabel="Preview" Style="text-align:center">
147147
<MudFab Color="Color.Primary" StartIcon="@Icons.Material.Filled.Pageview"
148-
Size="Size.Small" OnClick="@(() => OnShowDocument(context))" />
148+
Size="Size.Small" OnClick="@(() => OnShowDocumentAsync(context))" />
149149
</MudTd>
150150
<MudTd DataLabel="Status" Style="text-align:center">
151151
@{

0 commit comments

Comments
 (0)