将静态服务中的列表添加到视图中的 MVVM



我创建了一个服务(静态方法(,例如,它将获取谷歌云端硬盘中的所有文件夹并返回一个List<File>。(这些方法是异步 MSDN 异步编程(

问题是我不知道如何将我的结果传递到视图中。我尝试使用可观察集合,但我无法使其工作。

还有一件事是,我不确定它在我的用法中是否有用。我不添加一个项目或删除一个项目。我只是每次刷新都废弃整个文件夹。我的理解是,这对于用户将编辑的数据集合很有用。

public class MainWindowViewModel : ViewModelBase
{
    public MainWindowViewModel()
    {
        var service = DriveHelper.createDriveService("client_secret.json", false)
        // ERROR
        _googleDriveFolders = new NotifyTaskCompletion<List<File>>( DriveHelper.getFiles(service), "trashed=false and mimeType = 'application/vnd.google-apps.folder'"));
    }
    public NotifyTaskCompletion<List<File>> googleDriveFolders { get; private set; }
    private ObservableCollection<File> _googleDriveFolders;
    public ObservableCollection<File> googleDriveFolder
    {
        get { return _googleDriveFolders; }
        set
        {
            _googleDriveFolders = value;
            RaisePropertyChanged();
        }
    }
//...

如注释中所述,问题是您的NotifyTaskCompletion会立即返回并分配给refreshFoldersCommand(顺便说一句,在 C# 命名约定中,属性在 Pascal Case 又名 Camel 大写中,而不是 Camel 小写表示法(属性,并且事件会立即引发,而不是在异步操作完成后

。将

async代码放入 ViewModels 构造函数(或在这种情况下的任何构造函数(是非常糟糕的做法,因为在构造函数中无法await async 方法。

没有简单的解决方案。正确的解决方案要求您更改应用程序体系结构并使用导航服务。我已经在StackOverflow上发布了几次。

Prism(Microsoft的MVVM框架(确实带有一个干净的解决方案。它有一个INavigationAware接口,包含3个方法(OnNagivatedToOnNavigatedFromIsNavigatioNTarget(。若要将数据异步加载到 ViewModel,NavigateTo很重要。

在 Prism 中,它是在卸载上一个视图之后调用的(在调用 NavigateFrom in the former ViewModels class) and the newly one has been instantiated and assigned to the new View. Parameters passed to the NavigationService.Navigate(..(method are passed to 视图模型的OnNagivatedTo"方法。

它可以标记为async,您可以将代码放在那里并等待它

public class MainWindowViewModel : ViewModelBase
{
    public MainWindowViewModel()
    {
    }
    public NotifyTaskCompletion<List<File>> googleDriveFolders { get; private set; }
    private ObservableCollection<File> _googleDriveFolders;
    public ObservableCollection<File> googleDriveFolder
    {
        get { return _googleDriveFolders; }
        set
        {
            _googleDriveFolders = value;
            RaisePropertyChanged();
        }
    }
    public async void OnNavigatedTo(NavigationContext context) 
    {
        var service = DriveHelper.createDriveService("client_secret.json", false)
        // ERROR
        googleDriveFolder = await DriveHelper.getFiles(service), "trashed=false and mimeType = 'application/vnd.google-apps.folder'");
    }
...
}

编辑:关于同一问题的进一步答案:将参数传递给视图模型中的构造函数

如何使用单击处理程序和命令在 WPF MVVM 中打开另一个视图?(我的解决方案合理吗?

编辑 2:此外,您将NotifyTaskCompletion分配给_googleDriveFolders这是googleDriveFolder属性的支持字段,因此永远不会调用RaisePropertyChanged();

**

编辑3:**从该教程中的代码开始,你的代码并不完全遵循本教程。教程中的家伙绑定到属性NotifyTaskCompletion。不过,您正在将其绑定到支持字段。

public MainWindowViewModel()
{
    var service = DriveHelper.createDriveService("client_secret.json", false)
    // your property is named googleDriveFolders, but you are assigning it to _googleDriveFolders
    googleDriveFolders = new NotifyTaskCompletion<List<File>>( DriveHelper.getFiles(service), "trashed=false and mimeType = 'application/vnd.google-apps.folder'"));
}

此代码完成后,不会调用RaisePropertyChanged("googleDriveFolder")(这是您的可观察列表(,因为NotifyTaskCompletion只会刷新它自己的属性。您很可能已将视图绑定到googleDriveFolder(可观察属性(而不是googleDriveFolders.Result

对于此示例,必须绑定到 googleDriveFolders.Result ,因为更改通知只会针对NotificationTaskCompletition对象的 Result 属性触发,如示例代码propertyChanged(this, new PropertyChangedEventArgs("Result"));所示。

因此,XAML 必须看起来像

<ListView Source="{Binding googleDriveFolders.Result}"/>

但无论如何,问题仍然存在,在构造函数中执行异步操作是一种不好的做法,因此即使在您的单元测试中,例如,每次初始化对象时,它也会启动异步任务,因此在每个 UnitTest 中,即使您测试不同的东西并且无法轻松地将参数传递给它(例如传递要加载的链接或文件夹名称(。

因此,干净的方法是通过导航服务和需要它的 ViewModel 的INavigationAware实现(不执行任何异步操作的模式不实现此接口(。

我得出了这个解决方案...但我认为这不是最好的方法。<.<使用列表视图。>

namespace UpdateUploader.ViewModels
{
using System.Windows.Input;
using Helper;
using Services;
using System.Collections;
using System.Collections.ObjectModel;
using Google.Apis.Drive.v2.Data;
using Google.Apis.Drive.v2;
using System.Collections.Generic;
public class MainWindowViewModel : ViewModelBase
{
    DriveService _service;
    public MainWindowViewModel()
    {
        _service = DriveHelper.createDriveService("client_secret.json", false);
        googleDriveFolders = new NotifyTaskCompletion<List<File>>( DriveHelper.getFiles(_service, "trashed=false and mimeType = 'application/vnd.google-apps.folder'"));
    }
    public NotifyTaskCompletion<List<File>> _googleDriveFolders;
    public NotifyTaskCompletion<List<File>> googleDriveFolders
    {
        get { return _googleDriveFolders; }
        set
        {
            _googleDriveFolders = value;
            RaisePropertyChanged();
        }
    }
    #region ICommands
    private ICommand _refreshFoldersCommand;
    public ICommand refreshFoldersCommand
    {
        get
        {
            if (this._refreshFoldersCommand == null)
            {
                _refreshFoldersCommand = new RelayCommand(p => this.loadFolders(p));
            }
            return this._refreshFoldersCommand;
        }
    }
    #endregion ICommands
    public void loadFolders(object parameter)
    {
        googleDriveFolders = new NotifyTaskCompletion<List<File>>(DriveHelper.getFiles(_service, "trashed=false and mimeType = 'application/vnd.google-apps.folder'"));
    }
}
}

编辑

通知任务完成.cs

namespace UpdateUploader.Helper
{
    using System;
    using System.ComponentModel;
    using System.Threading.Tasks;
    public sealed class NotifyTaskCompletion<TResult> : INotifyPropertyChanged
    {
        public NotifyTaskCompletion(System.Threading.Tasks.Task<TResult> task)
        {
            Task = task;
            if (!task.IsCompleted)
            {
                var _ = WatchTaskAsync(task);
            }
        }
        private async Task WatchTaskAsync(Task task)
        {
            try
            {
                await task;
            }
            catch
            {
            }
            var propertyChanged = PropertyChanged;
            if (propertyChanged == null)
                return;
            propertyChanged(this, new PropertyChangedEventArgs("Status"));
            propertyChanged(this, new PropertyChangedEventArgs("IsCompleted"));
            propertyChanged(this, new PropertyChangedEventArgs("IsNotCompleted"));
            if (task.IsCanceled)
            {
                propertyChanged(this, new PropertyChangedEventArgs("IsCanceled"));
            }
            else if (task.IsFaulted)
            {
                propertyChanged(this, new PropertyChangedEventArgs("IsFaulted"));
                propertyChanged(this, new PropertyChangedEventArgs("Exception"));
                propertyChanged(this, new PropertyChangedEventArgs("InnerException"));
                propertyChanged(this, new PropertyChangedEventArgs("ErrorMessage"));
            }
            else
            {
                propertyChanged(this, new PropertyChangedEventArgs("IsSuccessfullyCompleted"));
                propertyChanged(this, new PropertyChangedEventArgs("Result"));
            }
        }
        public Task<TResult> Task { get; private set; }
        public TResult Result
        {
            get { return (Task.Status == TaskStatus.RanToCompletion) ? Task.Result : default(TResult); }
        }
        public TaskStatus Status { get { return Task.Status; } }
        public bool IsCompleted { get { return Task.IsCompleted; } }
        public bool IsNotCompleted { get { return !Task.IsCompleted; } }
        public bool IsSuccessfullyCompleted
        {
            get
            {
                return Task.Status == TaskStatus.RanToCompletion;
            }
        }
        public bool IsCanceled { get { return Task.IsCanceled; } }
        public bool IsFaulted { get { return Task.IsFaulted; } }
        public AggregateException Exception { get { return Task.Exception; } }
        public Exception InnerException
        {
            get
            {
                return (Exception == null) ? null : Exception.InnerException;
            }
        }
        public string ErrorMessage
        {
            get
            {
                return (InnerException == null) ? null : InnerException.Message;
            }
        }
        public event PropertyChangedEventHandler PropertyChanged;
    }
}
DriveService

.cs(摘录; createDriveService, getfiles(

namespace UpdateUploader.Services
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Threading;
    using System.Threading.Tasks;
    using Google.Apis.Auth.OAuth2;
    using Google.Apis.Drive.v2;
    using Google.Apis.Drive.v2.Data;
    using Google.Apis.Services;
    using Google.Apis.Util.Store;
    using Google.Apis.Upload;
    class DriveHelper
    {
        private static bool _unique;
        public static DriveService createDriveService(string passFilePath, bool createUniqueID)
        {
            _unique = createUniqueID;
            if (!System.IO.File.Exists(passFilePath))
            {
                Console.Error.WriteLine("keyfile not found...");
                return null;
            }
            string[] scopes = new string[] { DriveService.Scope.Drive }; // Full accces
            // loading the key file
            UserCredential credential;
            using (var stream = new System.IO.FileStream("client_secret.json", System.IO.FileMode.Open, System.IO.FileAccess.Read))
            {
                string credPath = System.Environment.GetFolderPath(
                    System.Environment.SpecialFolder.Personal);
                credPath = System.IO.Path.Combine(credPath, ".credentials/update-uploader");
                credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
                    GoogleClientSecrets.Load(stream).Secrets,
                    scopes,
                    "user",
                    CancellationToken.None,
                    new FileDataStore(credPath, true)).Result;
                Console.WriteLine("Credential file saved to: " + credPath);
            }
            var service = new DriveService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = "Update Uploader",
            });
            return service;
        }
        // search = null ; get all files/folders
        public static async Task<List<File>> getFiles(DriveService service, string search)
        {
            System.Collections.Generic.List<File> Files = new System.Collections.Generic.List<File>();
            try
            {
                // list all files with max 1000 results
                FilesResource.ListRequest list = service.Files.List();
                list.MaxResults = 1000;
                if (search != null)
                {
                    list.Q = search;
                }
                FileList filesFeed = await list.ExecuteAsync();
                while (filesFeed.Items != null)
                {
                    foreach (File item in filesFeed.Items)
                    {
                        Files.Add(item);
                    }
                    // if it is the last page break
                    if (filesFeed.NextPageToken == null)
                    {
                        break;
                    }
                    list.PageToken = filesFeed.NextPageToken;
                    filesFeed = list.Execute();
                }
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ex.Message);
            }
            return Files;
        }

视图模型库.cs

namespace UpdateUploader.Helper
{
using System.ComponentModel;
using System.Runtime.CompilerServices;
public abstract class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
    {
        // new since 4.6 or 4.5
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

}

最新更新