我正在开发一个爬虫程序,我希望它有与浏览器类似的体验,即我可以后退/前进,我想使用 Frame 控件,但我不知道该怎么做,我的视图是相同的,每个查询只是一个不同的虚拟机。
我尝试自定义 INavigationService 及其实现,基本上工作正常。但是由于像 QueryRequest 这样的对象是引用类型,它们在导航时无法维护自己的状态,一种解决方案是使用深度克隆,但该对象可能会订阅事件,而深度克隆似乎不会克隆事件,无论如何,我需要维护整个页面状态。
如果我使用Frame控件,我不需要管理页面状态,但它似乎不符合逻辑,当我单击查询按钮(QueryCommand)时如何导航到新页面并执行查询?假设我这样做,需要调用nextView/ViewModel中的Query,然后就会进入无限循环!
另一件让我困惑的事情是,如果我在浏览器中使用 Google 搜索“Foo”,然后搜索“Bar”,然后返回,为什么搜索框会记住“Foo”?我如何在 WPF/Frame 中执行此操作?
services.AddTransient(); services.AddTransient();
导航服务:
public class NavigationService<T> : INavigationService<T> where T : notnull
{
public Stack<T> BackStack { get; } = [];
public bool CanGoBack => BackStack.Count > 1;
public bool CanGoForward => ForwardStack.Count > 0;
public T? Current { get; private set; }
public Stack<T> ForwardStack { get; } = [];
public event EventHandler<NavigationEventArgs<T>>? Navigated;
public T? GoBack(int step = 1)
{
for (int i = 0; i < step; i++)
{
if (CanGoBack)
{
var current = BackStack.Pop();
ForwardStack.Push(current);
}
}
if (BackStack.TryPeek(out var previous))
{
Current = previous;
}
OnNavigated();
return Current;
}
public void GoBackTo(T content)
{
if (BackStack.Contains(content))
{
if (!EqualityComparer<T>.Default.Equals(BackStack.Peek(), content))
{
GoBack();
}
}
}
public T? GoForward(int step = 1)
{
for (int i = 0; i < step; i++)
{
if (CanGoForward)
{
var next = ForwardStack.Pop();
BackStack.Push(next);
Current = next;
}
}
OnNavigated();
return Current;
}
public void GoForwardTo(T content)
{
if (BackStack.Contains(content))
{
if (!EqualityComparer<T>.Default.Equals(ForwardStack.Peek(), content))
{
GoForward();
}
}
}
public void Navigate(T content, bool replace = false)
{
if (replace)
{
BackStack.Pop();
}
BackStack.Push(content);
Current = content;
ForwardStack.Clear();
OnNavigated();
}
protected virtual void OnNavigated()
{
Navigated?.Invoke(this, new(Current!, BackStack, ForwardStack));
}
}
查询视图模型:
// Bind to Query button command.
[RelayCommand(IncludeCancelCommand = true)]
private async Task QueryAsync(CancellationToken cancellationToken = default)
{
try
{
using var scope = _serviceScopeFactory.CreateScope();
var webService = scope.ServiceProvider.GetRequiredService<IWebService>();
QueryResponse = await webService.QueryAsync(QueryRequest, cancellationToken);
}
catch (TaskCanceledException)
{
Growl.Info("Cancelled.");
}
catch
{
QueryResponse = null;
throw;
}
finally
{
_navigationService.Navigate(new QuerySession(
PageIndex,
PageSize,
QueryRequest,
QueryResponse,
VerticalScrollOffset), _refreshMode);
}
}
// Bind to GoBack button command.
[RelayCommand]
private void GoBack()
{
_isNavigating = true;
var session = _navigationService.GoBack();
if (session is not null)
{
PageIndex = session.PageIndex;
PageSize = session.PageSize;
QueryRequest = session.QueryRequest;
QueryResponse = session.QueryResponse;
VerticalScrollOffset = session.VerticalScrollOffset;
}
_isNavigating = false;
}
// Omitted GoForward、Refresh...
受到@Sir Rufo的启发,我实现了自己的版本(肮脏,可能不适合所有人):
我的Page是
KeepAlive=True
,如果Frame备份然后重新导航,它就会从ForwardStack中清除,但Page/Vm似乎没有被处置。所以我现在正在努力修复 ForwardStack 为空时的内存泄漏。
public class FrameX : Frame
{
public FrameX()
{
Navigated += FrameX_Navigated;
}
private static void SetValue<T, TValue>(T obj, string propertyName, TValue value) where T : notnull
{
var type = typeof(T);
if (type.IsInterface)
{
type = obj.GetType();
}
var backingField = type.GetField(
$"<{propertyName}>k__BackingField",
BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)
?? throw new ArgumentException($"Property {propertyName} not found in type {type.FullName}.");
backingField.SetValue(obj, value);
}
private void FrameX_Navigated(object sender, NavigationEventArgs e)
{
var navigatablePage = (NavigatablePage)e.Content;
if (!navigatablePage.IsNavigated)
{
SetValue(navigatablePage, nameof(NavigatablePage.NavigationEventArgs), e);
var navigator = (INavigator)navigatablePage.DataContext;
SetValue(navigator, nameof(INavigator.NavigationService), NavigationService);
}
}
}
public interface INavigatablePage
{
bool IsNavigated { get; }
bool IsNavigating { get; }
NavigationEventArgs NavigationEventArgs { get; }
INavigator Navigator { get; }
}
// KeepAlive="True" in xaml
public abstract class NavigatablePage : Page, INavigatablePage
{
public bool IsNavigated { get; protected set; }
public bool IsNavigating { get; protected set; }
public NavigationEventArgs NavigationEventArgs { get; } = null!;
public INavigator Navigator { get; }
protected NavigatablePage()
{
Navigator = App.Current.Services.GetRequiredService<INavigator>();
DataContext = Navigator;
Loaded += NavigatablePage_Loaded;
Unloaded += NavigatablePage_Unloaded;
}
protected virtual async Task NavigatedToAsync(NavigationEventArgs e)
{
if (!IsNavigated)
{
IsNavigating = true;
try
{
IsNavigated = true;
await Navigator.OnNavigatedToAsync(e);
}
finally
{
IsNavigating = false;
}
}
}
protected virtual async Task OnLoadedAsync(object sender, RoutedEventArgs e)
{
await NavigatedToAsync(NavigationEventArgs);
}
protected virtual async Task OnUnloadedAsync(object sender, RoutedEventArgs e)
{
Loaded -= NavigatablePage_Loaded;
Unloaded -= NavigatablePage_Unloaded;
await Task.CompletedTask;
}
private async void NavigatablePage_Loaded(object sender, RoutedEventArgs e)
{
await OnLoadedAsync(sender, e);
}
private async void NavigatablePage_Unloaded(object sender, RoutedEventArgs e)
{
await OnUnloadedAsync(sender, e);
}
}
用途:
public partial class QueryViewModel : INavigator
{
public async Task OnNavigatedToAsync(NavigationEventArgs navigationState)
{
// Perform query.
}
private void OnQueryButtonClickCommand()
{
NavigationService.Navigate(new DerivedNavigatablePage(), navigationState);
}
}