如何使我的构造函数在 UWP MVVM 模型中异步? (MVVM Lighttoolkit)

How to make my constructor async in UWP MVVM model? (MVVM Lighttoolkit)

我有一个 UWP 项目想要读取 StorageFolder VideosLibrary 并在带有缩略图的视图中显示 mp4 文件列表。

使用 MVVM ligth 工具包,我已经用 xaml 设置了这 4 只苍蝇。 Xaml 正在使用 UWP 社区工具包包装面板。

1)ViewModelLocator.cs

namespace UWP.ViewModels
{
/// <summary>
/// This class contains static reference to all the view models in the 
/// application and provides an entry point for the bindings.
/// </summary>

class ViewModelLocator
{
    /// <summary>
    /// Initializes a new instance of the ViewModelLocator class.
    /// </summary>
    public ViewModelLocator()
    {
        ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
        if (ViewModelBase.IsInDesignModeStatic)
        {
            // Create  design time view services and models
        }
        else
        {
            // Create run Time view services and models
        }
        //Register services used here

        SimpleIoc.Default.Register<VideoListModel>();
    }


    public VideoListModel VideoListModel
    {
        get { return ServiceLocator.Current.GetInstance<VideoListModel>(); 
    }
}
}

2) VideoListItem.cs

namespace UWP.Models
{
class VideoListItem : ViewModelBase
{
    public string VideoName { get; set; }
    public string Author { get; set; }
    public Uri Vid_url { get; set; }
    public BitmapImage Image { get; set; }

    public VideoListItem(string videoname,string author,Uri url, BitmapImage img)
    {
        this.VideoName = videoname;
        this.Author = author;
        this.Vid_url = url;
        this.Image = img;
    }
}
}

3) VideoListModel.cs

namespace UWP.ViewModels
{
class VideoListModel : ViewModelBase
{
    public ObservableCollection<VideoListItem> VideoItems { get; set; }

    private VideoListItem videoItems;

    public VideoListModel()
    {

    }

    public async static Task<List<VideoListItem>> GetVideoItem()
    {
        List<VideoListItem> videoItems = new List<VideoListItem>();
        StorageFolder videos_folder = await KnownFolders.VideosLibrary.CreateFolderAsync("Videos");
        var queryOptions = new QueryOptions(CommonFileQuery.DefaultQuery, new[] { ".mp4" });
        var videos = await videos_folder.CreateFileQueryWithOptions(queryOptions).GetFilesAsync();


        foreach (var video in videos)
        {
            //Debug.WriteLine(video.Name);
            //videoItems.Add(new VideoListItem());
            var bitmap = new BitmapImage();
            var thumbnail = await video.GetThumbnailAsync(ThumbnailMode.SingleItem);
            await bitmap.SetSourceAsync(thumbnail);
            videoItems.Add(new VideoListItem(video.DisplayName, "", new Uri(video.Path),bitmap));

        }

        //foreach(var video in videoItems)
        //{
        //    Debug.WriteLine("Name:{0} , Author:{1}, Uri:{2}, Bitmap:{3}", video.VideoName, video.Author, video.Vid_url, video.Image.UriSource);
        //}


        return videoItems;
    }


}
}

4) Video.xaml

<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="using:UWP.Views"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:Controls="using:Microsoft.Toolkit.Uwp.UI.Controls"
  x:Class="UWP.Views.Video"
  mc:Ignorable="d"
  NavigationCacheMode="Enabled"
  DataContext="{Binding Source={StaticResource ViewModelLocator},Path=VideoListModel}">
<!--NavigationCacheMode Enable for the page state save-->
<Page.Resources>
    <DataTemplate x:Key="VideoTemplate">
        <Grid Width="{Binding Width}"
              Height="{Binding Height}"
              Margin="2">
            <Image HorizontalAlignment="Center"
                   Stretch="UniformToFill"
                   Source="{Binding Image}" />
            <TextBlock Text="{Binding VideoName}"/>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Author" />
                <TextBlock Text="{Binding Author}" />
            </StackPanel>
        </Grid>
    </DataTemplate>
</Page.Resources>

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <ListView Name="VideosListWrapPanal"
              ItemTemplate="{StaticResource VideoTemplate}">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <Controls:WrapPanel />
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
    </ListView>

</Grid>
</Page>

我想在构造函数的 VideoListModel 中执行类似下面的操作。

public async MainViewModel()
{
   VideoItems = new ObservableCollection<MainMenuItem>(await GetVideoItem());

}

如何以异步方式完成此初始化? 为了获取缩略图,我创建了 GetVideoItem() 方法, 但是我找不到在构造函数中异步调用 GetVideoItem 的方法。 有谁知道如何解决这个任务?

我遇到了类似的问题,我做了这样的事情:

public MainViewModel()
{
GetVideoItem().ContinueWith(result=>{VideoItems = new ObservableCollection<MainMenuItem>(result)});


}

您也可以为此放置一个 loadingData 变量,让用户知道数据正在加载。

    public IsLoading{get;set}=true;
    public MainViewModel()
    {
    GetVideoItem().ContinueWith(result=>{VideoItems = new ObservableCollection<MainMenuItem>(result);
                                IsLoading=false;});
    }

简短的回答是:你不能创建构造函数 async

但是有一些选项可以解决这个问题。这里有两个建议:

解决方案 1:ViewModel 生命周期

很多MVVM框架使用lifecylce方法来解决这个问题。您可以添加一个 ActivateAsync 方法,该方法在实例化 ViewModel.

后由您的框架调用

在您的示例中,这可以在您的 ViewModelLocator 中完成。

interface IActivate
{
    Task ActivateAsync();
}

// Call it like this:
(model as IActivate)?.ActivateAsync(); // this will work even if the model does not implement IActivate

解决方案 2:使用工厂

另一种选择是使用 factory method 创建 ViewModelfactory method 可以获取所有 async 数据并在所有数据聚合后创建对象。

public static async Task<CustomViewModel> Create()
{
    var data = await FetchAsyncData();
    return new CustomViewModel(data);
}

示例:

这里是关于如何使用 activate 模式的简短片段。

public class ViewModelLocator 
{
    // existing implementation goes here

    public async Task<TViewModel> Create<TViewodel>
    {
        var model = ServiceLocator.Current.GetInstance<TViewodel>(); 
        var activate = model as IActivate;
        if(activate != null)
            await activate.ActivateAsync();

        return model;
    }
}

现在的工厂方法returns只是一个完全启动的模型。这种模式的优点是创建者不需要知道它正在创建的模型。它检查模型是否需要激活并调用它。然后可以将所有激活逻辑放在 ViewModel.

终于找到显示视频列表的方法了!

我错误地没有设置 ListView ItemSource! 虽然它仍然有Type not found in cache:UWP.Services.IKnownFolderReader的错误,但我认为通过加载文件启动应用程序时它会消失。

这是我的最终代码。

1) Page.xaml

<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="using:UWP.Views"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:Controls="using:Microsoft.Toolkit.Uwp.UI.Controls"
  x:Class="UWP.Views.Video"
  mc:Ignorable="d"
  NavigationCacheMode="Enabled"
  DataContext="{Binding Source={StaticResource ViewModelLocator},Path=VideoListModel}"
  Loaded="Page_Loaded">
<!--NavigationCacheMode Enable for the page state save-->
<Page.Resources>
    <DataTemplate x:Key="VideoTemplate">
        <Grid Width="{Binding Width}"
              Height="{Binding Height}"
              Margin="2">
            <Image HorizontalAlignment="Center"
                   Height="200"
                   Width="200"
                   Source="{Binding Image}" />
            <TextBlock Text="{Binding VideoName}"/>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Author" />
                <TextBlock Text="{Binding Author}" />
            </StackPanel>
        </Grid>
    </DataTemplate>
</Page.Resources>

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <ListView Name="VideosListWrapPanal"
              ItemsSource="{Binding VideoItems}"
              ItemTemplate="{StaticResource VideoTemplate}">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <Controls:WrapPanel Background="LightBlue"/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
    </ListView>

    <!--<StackPanel Orientation="Vertical"
                VerticalAlignment="Center"
                HorizontalAlignment="Center">
        <Viewbox MaxHeight="100"
                 MaxWidth="100">
            <SymbolIcon Symbol="Video" />
        </Viewbox>
        <TextBlock TextAlignment="Center"
                   Text="Home"
                   Margin="0,15,0,0" />

    </StackPanel>-->

</Grid>
</Page>

2) 型号

  public string VideoName { get; set; }
    public string Author { get; set; }
    public Uri Vid_url { get; set; }
    public BitmapImage Image { get; set; }

    public VideoListItem(string videoname,string author,Uri url, BitmapImage img)
    {
        this.VideoName = videoname;
        this.Author = author;
        this.Vid_url = url;
        this.Image = img;
    }

3) 视图模型

class VideoListModel : ViewModelBase
{
    private IKnownFolderReader _knownFolder;

    private ObservableCollection<VideoListItem> _videoItems;
    public ObservableCollection<VideoListItem> VideoItems
    {
        get { return _videoItems; }
        set { Set(ref _videoItems, value); RaisePropertyChanged(); }
    }



    private VideoListItem _selectedVideoItem;

    public VideoListItem SelectedVideoItem
    {
        get { return _selectedVideoItem; }
        set { Set(ref _selectedVideoItem, value);}
    }


    public VideoListModel(IKnownFolderReader knownFolder)
    {
        _knownFolder = knownFolder;
        var task = _knownFolder.GetData();
        task.ConfigureAwait(true).GetAwaiter().OnCompleted(() => {
            List<VideoListItem> items = task.Result;
            VideoItems = new ObservableCollection<VideoListItem>(items);
        });
    }



}

4) IKnownFolderReader.cs

public interface IKnownFolderReader
{
    Task<List<VideoListItem>> GetData();
}

5) VideoFilesReader.cs

public class VideoFilesReader : IKnownFolderReader
{
    private VideoListItem videoItems;
    public async Task<List<VideoListItem>> GetData()
    {
        List<VideoListItem> videoItems = new List<VideoListItem>();
        StorageFolder videos_folder = await KnownFolders.VideosLibrary.GetFolderAsync("Videos");
        var queryOptions = new QueryOptions(CommonFileQuery.DefaultQuery, new[] { ".mp4" });
        var videos = await videos_folder.CreateFileQueryWithOptions(queryOptions).GetFilesAsync();


        foreach (var video in videos)
        {
            var bitmap = new BitmapImage();
            var thumbnail = await video.GetThumbnailAsync(ThumbnailMode.SingleItem);
            await bitmap.SetSourceAsync(thumbnail);
            videoItems.Add(new VideoListItem(video.DisplayName, "", new Uri(video.Path), bitmap));

        }

        return videoItems;
    }
}

注意视图部分!

要从文件夹进行异步读取,我了解到应该将服务与 接口

分开

谢谢大家的帮助!我应该多研究一下 MVVM 模式,MVVM light 工具包和 UWP 开发,以防止再次出现这样的错误!

我建议使用异步任务通知程序,如我在 async MVVM data binding 上的文章中所述。

例如,使用 this helper library 中的 NotifyTask

public NotifyTask<List<VideoListItem>> VideoItems { get; }

public VideoListModel(IKnownFolderReader knownFolder)
{
  _knownFolder = knownFolder;
  VideoItems = NotifyTask.Create(() => _knownFolder.GetData());
}

您的数据绑定将从 ItemsSource="{Binding VideoItems}" 更改为 ItemsSource="{Binding VideoItems.Result}"。此外,VideoItems 还有其他几个属性,例如 IsNotCompletedIsFaulted,因此您的数据绑定可以根据任务的状态 show/hide 个元素。

这种方法避免了微妙的 problems with Result and problems with ContinueWith