Создать список недавно использованных (MRU) WinUI3/MVVM в строке меню

Я хотел бы создать классический список «Последние файлы» в строке меню моего приложения Windows (аналогично строке меню Visual Studio -> Файл -> Последние файлы -> см. список последних файлов)

Список MRU (List < string > myMRUList...) известен и не находится в центре внимания этого вопроса. Проблема заключается в том, как отображать и связывать/взаимодействовать со списком в соответствии с правилами MVVM.

Класс меню Microsoft.Toolkit.Uwp.UI.Controls будет удален в будущем выпуске, и они рекомендуют использовать элемент управления MenuBar из WinUI. Я не нашел примеров, которые используют MenuBar WinUI для создания списка «Последние файлы».

Я использую Template Studio для создания приложения WinUI 3. В ShellPage.xaml я добавил

<MenuFlyoutSubItem x:Name = "mruFlyout" Text = "Recent Files"></MenuFlyoutSubItem> 

и в ShellPage.xaml.c

private void Button_Click(object sender, RoutedEventArgs e)
{
   mruFlyout.Items.Insert(mruFlyout.Items.Count, new MenuFlyoutItem(){ Text = "C:\\Test1_" + DateTime.Now.ToString("MMMM dd") } );
   mruFlyout.Items.Insert(mruFlyout.Items.Count, new MenuFlyoutItem(){ Text = "C:\\Test2_" + DateTime.Now.ToString("MMMM dd") } );
   mruFlyout.Items.Insert(mruFlyout.Items.Count, new MenuFlyoutItem(){ Text = "C:\\Test3_" + DateTime.Now.ToString("MMMM dd") } );
} 

зная, что это не MVVM, но даже этот подход не работает должным образом, потому что динамически сгенерированный MenuFlyoutItem может быть обновлен только один раз с помощью события Button_Click().

Может ли кто-нибудь дать мне пример, как создать функциональность «Последние файлы», но любая помощь будет отличной! Спасибо

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
121
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

К сожалению, кажется, что нет лучшего решения, чем обрабатывать это в коде позади, поскольку коллекция Items доступна только для чтения и также не реагирует на изменения в макете пользовательского интерфейса.

В дополнение к этому обратите внимание, что из-за https://github.com/microsoft/microsoft-ui-xaml/issues/7797 обновление коллекции Items не отражается до тех пор, пока всплывающее окно не будет закрыто и повторно открыто.

Итак, если ваша ViewModel имеет ObservableCollection, я бы, вероятно, сделал это:

// 1. Register collection changed
MyViewModel.RecentFiles.CollectionChanged += RecentFilesChanged;

// 2. Handle collection change
private void RecentFilesChanged(object sender, NotifyCollectionChangedEventArgs args)
{
    // 3. Create new UI collection
    var flyoutItems = list.Select(entry =>
        new MenuFlyoutItem()
        {
            Text = entry.Name
        }
    );

    // 4. Updating your MenuFlyoutItem
    mruFlyout.Items.Clear();
    flyoutItems.ForEach(entry => mruFlyout.Items.Add(entry));
}

Спасибо за ответ. К сожалению, Items не имеет установщика, и VS выдает сообщение об ошибке «Невозможно назначить« Binding »в свойство «Items» только для чтения».

Zulu 08.01.2023 21:01

Вы совершенно правы, не думал, что так будет. Я обновил свой ответ, но, к сожалению, он не очень элегантный, поскольку API MenuBar не идеален для этого варианта использования.

chingucoding 08.01.2023 21:57

Может не работать. Кажется, возникает проблема при попытке изменить MenuFlyoutSubItems.

Andrew KeepCoding 10.01.2023 07:29

Да, действительно. Я обновил свой ответ, чтобы указать на это. Спасибо.

chingucoding 10.01.2023 18:45

Основываясь на ответе chingucoding, я получил привязку «последний список файлов».

Для полноты я публикую подробные фрагменты кода здесь (имейте в виду, что я не эксперт):

Снова используйте Template Studio для создания приложения WinUI 3.

ShellViewModel.cs

// constructor
public ShellViewModel(INavigationService navigationService, ILocalSettingsService localSettingsService)
{
    ...
    MRUUpdateItems();
}

ShellViewModel_RecentFiles.cs (<-- частичный класс)

using System.Collections.ObjectModel;
using System.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Windows.Storage;
using Windows.Storage.AccessCache;
using Windows.Storage.Pickers;

namespace App_MostRecentUsedTest.ViewModels;

public partial class ShellViewModel : ObservableRecipient
{
    public ObservableCollection<MRUItem> MRUItems{ get; set;} = new();
    
    // update ObservableCollection<MRUItem>MRUItems from MostRecentlyUsedList
    public void MRUUpdateItems()
    {
        var mruTokenList = StorageApplicationPermissions.MostRecentlyUsedList.Entries.Select(entry => entry.Token).ToList();
        var mruMetadataList = StorageApplicationPermissions.MostRecentlyUsedList.Entries.Select(entry => entry.Metadata).ToList(); // contains path as string
    
        MRUItems.Clear(); var i = 0;
        foreach (var path in mruMetadataList)
        {
            MRUItems.Add(new MRUItem() { Path = path, Token = mruTokenList[i++] });
        }
    }
    
    
    // called if user selects a recent used file from menu bar list
    [RelayCommand]
    protected async Task MRULoadFileClicked(int? fileId)
    {
        if (fileId is not null)
        {
            var mruItem = MRUItems[(int)fileId];
    
            FileInfo fInfo = new FileInfo(mruItem.Path ?? "");
            if (fInfo.Exists)
            {
                StorageFile? file = await Windows.Storage.AccessCache.StorageApplicationPermissions.MostRecentlyUsedList.GetFileAsync(mruItem.Token);
                if (file is not null) 
                { 
                    Windows.Storage.AccessCache.StorageApplicationPermissions.MostRecentlyUsedList.Add(file, file.Path); // store file.Path into Metadata
                    MRUUpdateItems();
                    // LOAD_FILE(file);
                }
            }
            else
            {
            }
        }
        await Task.CompletedTask;
    }
    
    
    [RelayCommand]
    protected async Task MenuLoadFileClicked()
    {
        StorageFile? file = await GetFilePathAsync();
        if (file is not null)
        {
            Windows.Storage.AccessCache.StorageApplicationPermissions.MostRecentlyUsedList.Add(file, file.Path); // store file.Path into Metadata
            MRUUpdateItems();
            // LOAD_FILE(file);
        }
        await Task.CompletedTask;
    }
    
    // get file path with filePicker
    private async Task<StorageFile?> GetFilePathAsync()
    {
        FileOpenPicker filePicker = new();
        filePicker.FileTypeFilter.Add(".txt");
        IntPtr hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow); 
        WinRT.Interop.InitializeWithWindow.Initialize(filePicker, hwnd); 
        return await filePicker.PickSingleFileAsync();
    }
   
   
    public class MRUItem : INotifyPropertyChanged
    {
        private string? path;
        private string? token;

        public string? Path
        {
            get => path;
            set
            {
                path = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(path));
            }
        }
    
        public string? Token
        {
            get => token;
            set => token = value;
        }
    
        public event PropertyChangedEventHandler? PropertyChanged;
    }
}

ShellPage.xaml

<MenuBar>
    <MenuBarItem x:Name = "ShellMenuBarItem_File">
        <MenuFlyoutItem x:Uid = "ShellMenuItem_File_Load" Command = "{x:Bind ViewModel.MenuLoadFileClickedCommand}" />
        <MenuFlyoutSubItem x:Name = "MRUFlyout" Text = "Recent Files..." />
    </MenuBarItem>
</MenuBar>

ShellPage.xaml.cs

// constructor
public ShellPage(ShellViewModel viewModel)
{ 
    ...
    
    // MRU initialziation
    // assign RecentFilesChanged() to CollectionChanged-event
    ViewModel.MRUItems.CollectionChanged += RecentFilesChanged;
    // Add (and RemoveAt) trigger RecentFilesChanged-event to update MenuFlyoutItems 
    ViewModel.MRUItems.Add(new MRUItem() { Path = "", Token = ""});
    ViewModel.MRUItems.RemoveAt(ViewModel.MRUItems.Count - 1);
}



// MRU Handle collection change
private void RecentFilesChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
    // project each MRUItems list element into a new UI MenuFlyoutItem flyoutItems list
    var i = 0;
    var flyoutItems = ViewModel.MRUItems.Select(entry =>
        new MenuFlyoutItem()
        {
            Text = "  " + i.ToString() + " " + FilenameHelper.EllipsisString(entry.Path, 65),
            Command = ViewModel.MRULoadFileClickedCommand,
            CommandParameter = i++
        }
    ); 

    //// If you want to update the list while it is shown, 
    //// you will need to create a new FlyoutItem because of 
    //// https://github.com/microsoft/microsoft-ui-xaml/issues/7797

    // Create a new flyout and populate it
    var newFlyout = new MenuFlyoutSubItem();
    newFlyout.Text = MRUFlyout.Text;  // Text = "Recent Files...";

    // Updating your MenuFlyoutItem
    flyoutItems.ToList().ForEach(item => newFlyout.Items.Add(item));

    // Get index of old sub item and remove it
    var oldIndex = ShellMenuBarItem_File.Items.IndexOf(MRUFlyout);
    ShellMenuBarItem_File.Items.Remove(MRUFlyout);

    // Insert the new flyout at the correct position
    ShellMenuBarItem_File.Items.Insert(oldIndex, newFlyout);

    // Assign newFlyout to "old"-MRUFlyout
    MRUFlyout = newFlyout;
}

Другие вопросы по теме