Я хотел бы создать классический список «Последние файлы» в строке меню моего приложения 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().
Может ли кто-нибудь дать мне пример, как создать функциональность «Последние файлы», но любая помощь будет отличной! Спасибо





К сожалению, кажется, что нет лучшего решения, чем обрабатывать это в коде позади, поскольку коллекция 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));
}
Вы совершенно правы, не думал, что так будет. Я обновил свой ответ, но, к сожалению, он не очень элегантный, поскольку API MenuBar не идеален для этого варианта использования.
Может не работать. Кажется, возникает проблема при попытке изменить MenuFlyoutSubItems.
Да, действительно. Я обновил свой ответ, чтобы указать на это. Спасибо.
Основываясь на ответе 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;
}
Спасибо за ответ. К сожалению, Items не имеет установщика, и VS выдает сообщение об ошибке «Невозможно назначить« Binding »в свойство «Items» только для чтения».