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

4

6

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?

Cryptocrystalline answered 12/9, 2017 at 5:40 Comment(5)
Here's the article you need to read: msdn.microsoft.com/en-gb/magazine/…Tyndale
Why don't you use a delegate instead. It's not recommended to use async methods or functions in a constructor.Clein
Sorry for my lack knowledge about delegate, I used async method because of the CreateFolderAsync method and GetFilesAsync method in my GetVideoItem method. Would you give me some advice how can i modify my code?Cryptocrystalline
how about using command from the ``window loaded ` and make the command async. so it will load data asyncTenno
A constructor cannot be async. You could use, say, the Activated event. But consider the consequences, the user would be looking at a window without content for a while. And you have to make sure he can't do anything that requires the VideoItems member to be valid. You really want to do this before the window becomes visible. Consider an async factory method.Jansen
C
1

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.

Chiba answered 15/9, 2017 at 16:40 Comment(3)
Thank you for reply! I have tried your code by using the Noti.Mvvm.Async packages! It just work fine as the previous code i sent and made my code cleaner! Thanks! I learned that data binding not really must be ObservableCollection of the class! thank you very much! ! Thank you for helping!Cryptocrystalline
hi @Stephen Cleary! Although I can use the Noti.Mvvm.Async , but in my xaml page, it shows this error Severity Code Description Project File Line Suppression State Error Could not load file or assembly 'Nito.Mvvm.Async, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified. Do you have any idea about this?Cryptocrystalline
@Leow: That sounds like a bug in the Visual Studio XAML designer. I'd try posting on an MSDN forum. The VS team did recently do a lot of XAML work, so it's possible this is a temporary regression.Chiba
S
5

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.

Snowmobile answered 12/9, 2017 at 7:54 Comment(6)
I have refactored my code to public VideoListModel(List<VideoListItem> data) { VideoItems = new ObservableCollection<VideoListItem>(data); } public async static Task<VideoListModel> Create() { var data = await GetVideoItem(); return new VideoListModel(data); } In this case , How should i resolve my ViewModel in ViewModelLocator?Cryptocrystalline
I have never worked with the ServiceLocator, but there should be a way to pass constructor parameter to it. In any case, you can't use a property, because a getter is always sync. I'll update my answer, with an example for the Activate pattern, because I think this is the simpler one in your case.Snowmobile
Thanks for reply. I still not solved my problems. I have tried with public async Task<VideoListModel> Create() { var model = ServiceLocator.Current.GetInstance<VideoListModel>(); var activate = model as IActivate; if(activate != null) { await activate.ActivateAsync(); } return model; }Cryptocrystalline
In my model.... public async static Task<List<VideoListItem>> Create() { var data = await GetVideoItem(); return data; } public VideoListModel() { var task =Task.Run(Create); task.Wait(); task.Wait(); VideoItems = new ObservableCollection<VideoListItem>(task.Result); SelectedVideoItem = VideoItems.FirstOrDefault(); } GetVideoItem() is same as above But it's still not workingCryptocrystalline
Or maybe my interface not correct? public interface IActivate { Task ActivateAsync(); }Cryptocrystalline
I have refactored my code as this file following the MVVM light sample that I found in github created by qmatteoq with the sample name MVVMLight.Advanced But this time i get another type of error Type not found in cache:ProductNoteUWP.Services.IKnownFolderReader Is it the problem of my ViewModelLocator?I have two ViewModel, one for menu that works fine. And now trying the VideoList which have such errorCryptocrystalline
C
1

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!

Cryptocrystalline answered 12/9, 2017 at 13:33 Comment(0)
C
1

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.

Chiba answered 15/9, 2017 at 16:40 Comment(3)
Thank you for reply! I have tried your code by using the Noti.Mvvm.Async packages! It just work fine as the previous code i sent and made my code cleaner! Thanks! I learned that data binding not really must be ObservableCollection of the class! thank you very much! ! Thank you for helping!Cryptocrystalline
hi @Stephen Cleary! Although I can use the Noti.Mvvm.Async , but in my xaml page, it shows this error Severity Code Description Project File Line Suppression State Error Could not load file or assembly 'Nito.Mvvm.Async, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified. Do you have any idea about this?Cryptocrystalline
@Leow: That sounds like a bug in the Visual Studio XAML designer. I'd try posting on an MSDN forum. The VS team did recently do a lot of XAML work, so it's possible this is a temporary regression.Chiba
H
0

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;});
    }
Hanging answered 12/9, 2017 at 6:36 Comment(2)
I have try your method with public VideoListModel() { GetVideoItem().ContinueWith(result=>{VideoItems = new ObservableCollection<MainMenuItem>(result.Result)}); } However, it just passes through the VideoListModel() Constructor before the asynchronous load done.Cryptocrystalline
You cannot make a constructor async, this will go through and when the task is done it will update your VideoItems collectionHanging

© 2022 - 2024 — McMap. All rights reserved.