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

1.9k views Asked by At

I have a UWP project want to reads StorageFolder VideosLibrary and show a list of mp4 files at Views with thumbnail.

With MVVM ligth toolkit I have setup this 4 flies with xaml. The Xaml is using UWP community toolkit wrap panel.

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>

I wanted to do something like below in my VideoListModel for constructor.

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

}

how can I accomplish this initialization in an asynchronous way? To get the thumbnail I have created the method of GetVideoItem(), But I can't find a way to call the GetVideoItem asynchronously in a constructor. Does anyone know how to solve this task?

4

There are 4 answers

3
Stephen Cleary On BEST ANSWER

I recommend using an asynchronous task notifier, as described in my article on async MVVM data binding.

E.g., using NotifyTask from this helper library:

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

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

Your data binding would then change from ItemsSource="{Binding VideoItems}" to ItemsSource="{Binding VideoItems.Result}". In addition, VideoItems has several other properties such as IsNotCompleted and IsFaulted so that your data binding can show/hide elements based on the state of the task.

This approach avoids the subtle problems with Result and problems with ContinueWith.

6
Iqon On

The short answer is: you cant make a constructor async.

But there are options to solve this. Here are two proposals:

Solution 1: ViewModel lifecycle

A lot of MVVM Frameworks use lifecylce methods to solve this problem. You could add an ActivateAsync method, which is called by your framework after instantiating the ViewModel.

In your example this could be done in your ViewModelLocator.

interface IActivate
{
    Task ActivateAsync();
}

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

Solution 2: Use a Factory

Another option is to use a factory method for creating the ViewModel. The factory method could fetch all async data and create the object after all data was aggregated.

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

Example:

Here a short snippet on how you could use the activate pattern.

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;
    }
}

Now the factory method returns only a fully activated model. This pattern has the advantage, that the creator does not need to know the model it is creating. It checks if the model needs activation and calls it. All activation logic can then be placed in the ViewModel.

2
npo On

I had a similar problem to this i did something like this:

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


}

You can also put a loadingData variable for this to let the user know that the data is loading.

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

Finally I get the way to show the list of Videos!

By mistake I didn't set the ListView ItemSource! Although it still have the error of Type not found in cache:UWP.Services.IKnownFolderReader but this I think it will disappear when the application is started by loading the files.

Here is my Final Code.

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) Model

  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) ViewModel

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;
    }
}

Be careful with the View Part!

To make asynchrounous read from folder I learn that should separate the service with interface

Thank you everyone for helping! I should have to study more about MVVM pattern,MVVM light toolkit and UWP developments to prevent such mistake again!