Skip to content

Commit c7c9eba

Browse files
Merge pull request #129 from CodebreakerApp/51-navigation
51 navigation
2 parents 9cadebd + 863e017 commit c7c9eba

55 files changed

Lines changed: 1223 additions & 140 deletions

Some content is hidden

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

src/CodeBreaker.Avalonia/CodeBreaker.Avalonia/App.axaml.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
using Codebreaker.ViewModels;
66
using Codebreaker.ViewModels.Contracts.Services;
77
using Codebreaker.ViewModels.Services;
8+
using CodeBreaker.Avalonia.Contracts.Services.Navigation;
89
using CodeBreaker.Avalonia.Services;
10+
using CodeBreaker.Avalonia.Services.Navigation;
911
using CodeBreaker.Avalonia.Views;
1012
using CodeBreaker.Avalonia.Views.Pages;
1113
using Microsoft.Extensions.Configuration;
@@ -28,11 +30,14 @@ public App()
2830
builder.Configuration.AddAppSettingsJson();
2931

3032
// Services
33+
builder.Services.AddNavigation<AvaloniaNavigationService>(pages => pages
34+
.Configure<GamePage>("GamePage")
35+
.Configure<TestPage>("TestPage")
36+
.ConfigureInitialPage("GamePage"));
3137
builder.Services.Configure<GamePageViewModelOptions>(options => { });
3238
builder.Services.AddScoped<IInfoBarService, InfoBarService>();
3339
builder.Services.AddTransient<IDialogService, AvaloniaDialogService>();
3440
builder.Services.AddScoped<GamePageViewModel>();
35-
builder.Services.AddTransient<GamePage>();
3641
builder.Services.AddHttpClient<IGamesClient, GamesClient>(client =>
3742
{
3843
client.BaseAddress = new(builder.Configuration.GetRequired("ApiBase"));
@@ -60,12 +65,12 @@ public override void OnFrameworkInitializationCompleted()
6065
{
6166
desktop.MainWindow = new MainWindow()
6267
{
63-
Content = _host.Services.GetRequiredService<GamePage>()
68+
Content = new Shell()
6469
};
6570
}
6671
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
6772
{
68-
singleViewPlatform.MainView = new GamePage();
73+
singleViewPlatform.MainView = new Shell();
6974
}
7075

7176
base.OnFrameworkInitializationCompleted();
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using Avalonia.Controls;
2+
using Codebreaker.ViewModels.Contracts.Services;
3+
4+
namespace CodeBreaker.Avalonia.Contracts.Services.Navigation;
5+
6+
internal interface IAvaloniaNavigationService : INavigationService
7+
{
8+
ContentControl CurrentPage { get; }
9+
10+
void Initialize();
11+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Avalonia.Controls;
2+
3+
namespace CodeBreaker.Avalonia.Contracts.Services.Navigation;
4+
5+
internal interface IPageService
6+
{
7+
string InitialPageKey { get; }
8+
9+
UserControl GetInitialPage();
10+
11+
UserControl GetPage(string key);
12+
13+
public UserControl this[string key] => GetPage(key);
14+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using Avalonia.Controls;
2+
using CodeBreaker.Avalonia.Contracts.Services.Navigation;
3+
using CommunityToolkit.Mvvm.ComponentModel;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Threading.Tasks;
7+
8+
namespace CodeBreaker.Avalonia.Services.Navigation;
9+
10+
internal class AvaloniaNavigationService : ObservableObject, IAvaloniaNavigationService
11+
{
12+
private readonly IPageService _pageService;
13+
14+
private readonly Stack<string> _navigationStack = new (100);
15+
16+
private ContentControl _currentPage = new UserControl();
17+
18+
public AvaloniaNavigationService(IPageService pageService)
19+
{
20+
_pageService = pageService;
21+
_navigationStack.Push(pageService.InitialPageKey);
22+
}
23+
24+
public ContentControl CurrentPage
25+
{
26+
get => _currentPage;
27+
private set
28+
{
29+
if (value is null || value == _currentPage)
30+
return;
31+
32+
OnPropertyChanging(nameof(CurrentPage));
33+
_currentPage = value;
34+
OnPropertyChanged(nameof(CurrentPage));
35+
}
36+
}
37+
38+
public bool CanGoBack => _navigationStack.Count != 0;
39+
40+
public void Initialize()
41+
{
42+
CurrentPage = _pageService.GetInitialPage();
43+
}
44+
45+
public ValueTask<bool> GoBackAsync()
46+
{
47+
if (_navigationStack.TryPop(out _) && _navigationStack.TryPeek(out var key))
48+
return NavigateToAsync(key);
49+
50+
return ValueTask.FromResult(false);
51+
}
52+
53+
public ValueTask<bool> NavigateToAsync(string key, object? parameter = null, bool clearNavigation = false)
54+
{
55+
UserControl page;
56+
57+
try
58+
{
59+
page = _pageService[key];
60+
}
61+
catch (ArgumentException)
62+
{
63+
return ValueTask.FromResult(false);
64+
}
65+
66+
CurrentPage = page;
67+
68+
if (clearNavigation)
69+
_navigationStack.Clear();
70+
71+
_navigationStack.Push(key);
72+
return ValueTask.FromResult(true);
73+
}
74+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using Codebreaker.ViewModels.Contracts.Services;
2+
using CodeBreaker.Avalonia.Contracts.Services.Navigation;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using System;
5+
6+
namespace CodeBreaker.Avalonia.Services.Navigation;
7+
8+
internal static class NavigationServiceDIExtensions
9+
{
10+
public static IServiceCollection AddNavigation<TNavigationService>(this IServiceCollection services, Action<PageServiceBuilder> builder)
11+
where TNavigationService : class, IAvaloniaNavigationService, INavigationService
12+
{
13+
PageServiceBuilder pageServiceBuilder = new();
14+
builder(pageServiceBuilder);
15+
services.AddSingleton<IPageService>(pageServiceBuilder.Build());
16+
services.AddSingleton<IAvaloniaNavigationService, TNavigationService>();
17+
services.AddSingleton<INavigationService>(x => x.GetRequiredService<IAvaloniaNavigationService>());
18+
return services;
19+
}
20+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using Avalonia.Controls;
2+
using CodeBreaker.Avalonia.Contracts.Services.Navigation;
3+
using System;
4+
using System.Collections.Generic;
5+
6+
namespace CodeBreaker.Avalonia.Services.Navigation;
7+
8+
internal class PageService(Dictionary<string, Func<UserControl>> pages, string initialPageKey) : IPageService
9+
{
10+
public string InitialPageKey =>
11+
initialPageKey;
12+
13+
public UserControl GetInitialPage() =>
14+
GetPage(InitialPageKey);
15+
16+
public UserControl GetPage(string key)
17+
{
18+
Func<UserControl>? pageFactory;
19+
20+
lock (pages)
21+
if (!pages.TryGetValue(key, out pageFactory))
22+
throw new ArgumentException($"Page not found: {key}. Did you forget to call PageService.Configure?");
23+
24+
return pageFactory();
25+
}
26+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using Avalonia.Controls;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
6+
namespace CodeBreaker.Avalonia.Services.Navigation;
7+
8+
internal class PageServiceBuilder
9+
{
10+
private readonly Dictionary<string, Func<UserControl>> _pages = new();
11+
12+
private string? _initialPage;
13+
14+
public PageServiceBuilder Configure<TView>()
15+
where TView : UserControl, new() =>
16+
Configure<TView>(typeof(TView).Name);
17+
18+
public PageServiceBuilder Configure<TView>(string key)
19+
where TView : UserControl, new()
20+
{
21+
lock (_pages)
22+
{
23+
if (_pages.ContainsKey(key))
24+
throw new ArgumentException($"The key {key} is already configured in {nameof(PageService)}");
25+
26+
var pageFactory = () => new TView();
27+
28+
if (_pages.ContainsValue(pageFactory))
29+
throw new ArgumentException($"This type is already configured with key {_pages.First(p => p.Value == pageFactory).Key}");
30+
31+
_pages.Add(key, pageFactory);
32+
}
33+
34+
return this;
35+
}
36+
37+
public PageServiceBuilder ConfigureInitialPage(string key)
38+
{
39+
_initialPage = key;
40+
return this;
41+
}
42+
43+
public PageService Build()
44+
{
45+
if (_initialPage is null)
46+
throw new ArgumentNullException(nameof(_initialPage), $"The initial page must be configured. Did you call .{nameof(ConfigureInitialPage)}?");
47+
48+
return new(_pages, _initialPage);
49+
}
50+
}

src/CodeBreaker.Avalonia/CodeBreaker.Avalonia/Views/Pages/GamePage.axaml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,9 @@
1414
<UserControl.Resources>
1515
<converter:GameStatusToBooleanConverter x:Key="GameStatusToBooleanConverter" />
1616
</UserControl.Resources>
17-
<Grid RowDefinitions="Auto,Auto,*">
18-
<components:InfoBarArea Grid.Row="0" Margin="0,0,0,20"/>
19-
<components:GameResultDisplay Grid.Row="1" />
20-
<Grid Grid.Row="1"
17+
<Grid RowDefinitions="Auto,*,Auto">
18+
<components:GameResultDisplay Grid.Row="0" />
19+
<Grid Grid.Row="0"
2120
RowDefinitions="Auto,*"
2221
ColumnDefinitions="3*,1*"
2322
Margin="8"
@@ -32,12 +31,12 @@
3231
</Button>
3332
</Grid>
3433
<components:PegSelectionView
35-
Grid.Row="1"
34+
Grid.Row="0"
3635
IsVisible="{Binding GameStatus, Mode=OneWay, Converter={StaticResource GameStatusToBooleanConverter}, ConverterParameter=Cancelable}"
3736
Margin="55,0,0,15" />
3837
<ItemsControl
3938
IsVisible="{Binding GameStatus, Mode=OneWay, ConverterParameter=Running}"
40-
Grid.Row="2"
39+
Grid.Row="1"
4140
ItemsSource="{Binding GameMoves, Mode=OneWay}"
4241
ItemTemplate="{StaticResource PegsTemplate}">
4342
<ItemsControl.ItemsPanel>
@@ -46,5 +45,6 @@
4645
</ItemsPanelTemplate>
4746
</ItemsControl.ItemsPanel>
4847
</ItemsControl>
48+
<Button Grid.Row="2" Content="To TestPage" Click="ToTestPageButtonClicked" />
4949
</Grid>
5050
</UserControl>
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
using Avalonia.Controls;
2+
using Avalonia.Interactivity;
23
using Codebreaker.ViewModels;
4+
using Codebreaker.ViewModels.Contracts.Services;
35

46
namespace CodeBreaker.Avalonia.Views.Pages;
57

68
public partial class GamePage : UserControl
79
{
10+
private readonly INavigationService _navigationService;
11+
812
public GamePage()
913
{
10-
DataContext = App.Current.GetService<GamePageViewModel>(); ;
14+
DataContext = App.Current.GetService<GamePageViewModel>();
1115
InitializeComponent();
16+
_navigationService = App.Current.GetService<INavigationService>();
1217
}
1318

1419
public GamePageViewModel ViewModel => (GamePageViewModel)DataContext!;
20+
21+
private void ToTestPageButtonClicked(object? sender, RoutedEventArgs e)
22+
{
23+
_navigationService.NavigateToAsync("TestPage");
24+
}
1525
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<UserControl xmlns="https://github.com/avaloniaui"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
4+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
5+
xmlns:pages="using:CodeBreaker.Avalonia.Views.Pages"
6+
xmlns:components="using:CodeBreaker.Avalonia.Views.Components"
7+
xmlns:navigationServices="using:CodeBreaker.Avalonia.Services.Navigation"
8+
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
9+
x:Class="CodeBreaker.Avalonia.Shell"
10+
x:DataType="navigationServices:AvaloniaNavigationService">
11+
<Grid RowDefinitions="Auto, *">
12+
<components:InfoBarArea Grid.Row="0" Margin="0,0,0,20"/>
13+
<ContentPresenter Grid.Row="1" Content="{Binding CurrentPage, Mode=OneWay}" />
14+
</Grid>
15+
</UserControl>

0 commit comments

Comments
 (0)