refacto API prefix + nav.back
This commit is contained in:
parent
bd6ca9d11f
commit
6825f74308
78 changed files with 770 additions and 222 deletions
|
|
@ -350,4 +350,9 @@ public partial class App : Application
|
|||
|
||||
return window.NavRoot.PushAsync(page);
|
||||
}
|
||||
|
||||
internal async Task GoBackAsync()
|
||||
{
|
||||
await window.NavRoot.PopAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using PostIt.Services;
|
||||
using PostIt.Views;
|
||||
using Yavsc.Api.Client;
|
||||
|
||||
namespace PostIt.ViewModels;
|
||||
|
|
@ -106,7 +107,7 @@ public partial class AddCircleMemberDialogViewModel : ViewModelBase
|
|||
/// UI from firing an event with a null payload.
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
public void Add()
|
||||
public async Task AddAsync()
|
||||
{
|
||||
if (Selected is null)
|
||||
{
|
||||
|
|
@ -114,5 +115,14 @@ public partial class AddCircleMemberDialogViewModel : ViewModelBase
|
|||
return;
|
||||
}
|
||||
Confirmed?.Invoke(this, Selected);
|
||||
var app = App.Current as App;
|
||||
await app.GoBackAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task CloseAsync()
|
||||
{
|
||||
var app = App.Current as App;
|
||||
await app.GoBackAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,6 +119,17 @@ public partial class CirclesPageViewModel : ViewModelBase
|
|||
var directory = services.GetRequiredService<IUserDirectory>();
|
||||
AddCircleMemberDialogViewModel model =
|
||||
new AddCircleMemberDialogViewModel(directory);
|
||||
// Wire the dialog's Confirmed event to OnAddMemberConfirmedAsync.
|
||||
// Without this, the dialog's "Ajouter" button fires the event
|
||||
// into the void: no subscriber, the picked user is silently
|
||||
// dropped, and nothing is added to the circle. The dialog
|
||||
// stays open until the user uses the back gesture — which is
|
||||
// how the user noticed the button was a no-op.
|
||||
// Async-void is intentional here: Confirmed is an
|
||||
// EventHandler<T> (returns void), and bridging to the
|
||||
// async Task OnAddMemberConfirmedAsync requires it.
|
||||
model.Confirmed += async (_, picked) =>
|
||||
await OnAddMemberConfirmedAsync(_, picked);
|
||||
await app.PushPageAsync(model);
|
||||
}
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
|
@ -52,6 +51,22 @@ public partial class PostAclDialogViewModel : ViewModelBase
|
|||
[ObservableProperty]
|
||||
public partial string StatusMessage { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Idempotency gate for <see cref="LoadAsync"/>: the dialog
|
||||
/// attaches the load trigger in <c>DataContextChanged</c>,
|
||||
/// which can fire more than once if the page is detached
|
||||
/// and re-attached (dialog re-use, navigation edge cases)
|
||||
/// with a different VM. Without this guard, the second load
|
||||
/// would race against the first and could overwrite
|
||||
/// <see cref="AclEntries"/> mid-edit. Pattern copied from
|
||||
/// <c>Settings.Load</c>.
|
||||
/// </summary>
|
||||
private bool _loaded;
|
||||
|
||||
/// <summary>True once <see cref="LoadAsync"/> has run at least
|
||||
/// once. Exposed for tests; do not bind from XAML.</summary>
|
||||
public bool Loaded => _loaded;
|
||||
|
||||
public PostAclDialogViewModel(
|
||||
BlogPostDto post,
|
||||
BlogAclApiClient aclClient,
|
||||
|
|
@ -68,6 +83,8 @@ public partial class PostAclDialogViewModel : ViewModelBase
|
|||
[RelayCommand]
|
||||
public async Task LoadAsync()
|
||||
{
|
||||
if (_loaded) return;
|
||||
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
|
|
@ -83,6 +100,7 @@ public partial class PostAclDialogViewModel : ViewModelBase
|
|||
|
||||
|
||||
StatusMessage = $"{AclEntries.Count} autorisation(s)";
|
||||
_loaded = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
xmlns:services="using:PostIt.Services"
|
||||
x:DataType="vm:AddCircleMemberDialogViewModel"
|
||||
>
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto" Margin="12">
|
||||
|
||||
<!-- Search box + button -->
|
||||
|
|
@ -29,7 +30,9 @@
|
|||
<!-- Search results -->
|
||||
<ListBox Grid.Row="2"
|
||||
ItemsSource="{Binding Results}"
|
||||
SelectedItem="{Binding Selected, Mode=TwoWay}">
|
||||
SelectedItem="{Binding Selected, Mode=TwoWay}"
|
||||
MinHeight="20"
|
||||
>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="services:UserSummary">
|
||||
<StackPanel Spacing="2">
|
||||
|
|
@ -47,11 +50,13 @@
|
|||
<TextBlock Grid.Column="0" Text="{Binding StatusMessage}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="1" Content="Ajouter"
|
||||
Command="{Binding Add}"
|
||||
x:Name="AddButton"
|
||||
Command="{Binding AddAsync}"
|
||||
IsEnabled="{Binding Selected, Converter={x:Static ObjectConverters.IsNotNull}}"
|
||||
Margin="0,0,8,0"/>
|
||||
<Button Grid.Column="2" Content="Fermer"
|
||||
Click="OnCloseClicked"/>
|
||||
x:Name="CloseButton"
|
||||
Command="{Binding CloseAsync}"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ContentPage>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.VisualTree;
|
||||
using PostIt.Services;
|
||||
using PostIt.ViewModels;
|
||||
|
||||
|
|
@ -41,8 +42,8 @@ public partial class AddCircleMemberDialog : ContentPage
|
|||
|
||||
private void OnCloseClicked(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
// Same light-modal pattern as PostAclDialog: rely on
|
||||
// the system back gesture or the navigation host's
|
||||
// "pop" — the ContentPage doesn't own the back stack.
|
||||
var nav = this.FindAncestorOfType<NavigationPage>();
|
||||
if (nav is not null)
|
||||
_ = nav.PopAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using PostIt.ViewModels;
|
||||
|
|
@ -9,21 +10,53 @@ namespace PostIt.Views;
|
|||
/// <summary>
|
||||
/// Modal "manage ACL" page for a single blog post.
|
||||
///
|
||||
/// <para>The ViewModel is constructed here (not via DI) because it
|
||||
/// depends on the post being managed, which the caller (the post
|
||||
/// list page) only knows at the moment it opens the dialog. The
|
||||
/// DI container can build the two API clients; the post and the
|
||||
/// VM are wired together here.</para>
|
||||
/// <para>The ViewModel is constructed by the caller (the post
|
||||
/// list page) and handed to <see cref="App.PushPageAsync"/>,
|
||||
/// which routes through <see cref="ViewLocator"/> and lands
|
||||
/// here via the parameterless DI constructor. The VM is then
|
||||
/// assigned to <see cref="ContentPage.DataContext"/> by
|
||||
/// <c>App.PushPageAsync</c> — we listen for that one-shot
|
||||
/// assignment and trigger <c>LoadAsync</c> right after, so the
|
||||
/// dropdown's <c>MyCircles</c> and the list's <c>AclEntries</c>
|
||||
/// are populated when the dialog appears. The VM is idempotent
|
||||
/// under repeated loads.</para>
|
||||
/// </summary>
|
||||
public partial class PostAclDialog : ContentPage
|
||||
{
|
||||
public PostAclDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// App.PushPageAsync wires the VM via DataContext after
|
||||
// building the page. We subscribe once to fire LoadAsync
|
||||
// the moment the VM is attached. Using DataContextChanged
|
||||
// (rather than AttachedToVisualTree) is what makes this
|
||||
// work in the headless test harness too: the load is
|
||||
// tied to the VM being available, not to the visual tree
|
||||
// being realised (which is a separate concern).
|
||||
EventHandler? handler = null;
|
||||
handler = (_, _) =>
|
||||
{
|
||||
if (DataContext is PostAclDialogViewModel vm)
|
||||
{
|
||||
this.DataContextChanged -= handler;
|
||||
_ = vm.LoadAsync();
|
||||
}
|
||||
};
|
||||
this.DataContextChanged += handler;
|
||||
}
|
||||
|
||||
public PostAclDialog(BlogPostDto post, BlogAclApiClient aclClient, CircleApiClient circleClient)
|
||||
{
|
||||
// This overload is not used by the production path —
|
||||
// MainPageViewModel pushes the VM via App.PushPageAsync
|
||||
// and App routes through ViewLocator, which resolves this
|
||||
// page via the parameterless ctor. It is kept so test
|
||||
// scaffolding that wants to bypass the nav pipeline can
|
||||
// still wire a VM directly without losing the load
|
||||
// trigger: the constructor sets DataContext before the
|
||||
// DataContextChanged subscription fires, so the load
|
||||
// is guaranteed to run in either case.
|
||||
InitializeComponent();
|
||||
DataContext = new PostAclDialogViewModel(post, aclClient, circleClient);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue