如何在wpf中使用MVVM模式进行框架导航(相同视图,不同VM)?

问题描述 投票:0回答:1

我正在开发一个爬虫程序,我希望它有与浏览器类似的体验,即我可以后退/前进,我想使用 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...
c# wpf mvvm navigation frame
1个回答
0
投票

受到@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);
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.