Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 306
luvwinnie Avatar asked Sep 12 '17 05:09

luvwinnie


2 Answers

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.

like image 64
Iqon Avatar answered Sep 21 '22 23:09

Iqon


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.

like image 35
Stephen Cleary Avatar answered Sep 22 '22 23:09

Stephen Cleary