WinRT ViewModel DataBind to async method

2019-07-08 10:23发布

问题:

I am deserializing a list of objects from an XML file, and would like to bind to the actual content of those objects in my View, passing over a ViewModel. The problem is that file operations are async and this bubbles all the way up to the ViewModel, where Property getters cannot be marked as such...

Problem

  • I deserialize all XML files in a folder to Profile objects and store them in a List<Profile>. This method (has to be) marked async.

        public static async Task<List<Profile>> GetAllProfiles()
        {
            DataContractSerializer ser = new DataContractSerializer(typeof(Profile));
            StorageFolder folder = await ApplicationData.Current.RoamingFolder.CreateFolderAsync("Profiles", CreationCollisionOption.OpenIfExists);
    
            List<Profile> profiles = new List<Profile>();
            foreach (var f in await folder.GetFilesAsync())
            {
                var fs = await f.OpenStreamForReadAsync();
                profiles.Add((Profile)ser.ReadObject(fs));
               fs.Dispose();
            }
    
            return profiles;
        }
    

Ideal solution 1

  • The binding property in my ViewModel would then ideally call that static method like this

    public async Task<ObservableCollection<string>> Lists
    {
        get 
        {
            return new ObservableCollection<string>(GetAllProfiles().Select(p => p.Name));
        }
    }
    
  • BUT Properties cannot be marked async

Ideal solution 2

    public ObservableCollection<string> Lists
    {
        get 
        {
            return new ObservableCollection<string>((GetAllProfiles().Result).Select(p => p.Name));
        }
    }
  • BUT this never executes (it blocks in the await folder.GetFilesAsync() call for some reason)

Current solution

Calls an async Initialize() method that loads the result of the GetProfiles() function in a variable, and then makes a NotifyPropertyChanged("Lists") call:

    public ViewModel()
    {
        Initialize();
    }

    public async void Initialize()
    {
        _profiles = await Profile.GetAllProfiles();
        NotifyPropertyChanged("Lists");
    }

    private List<Profile> _profiles;
    public ObservableCollection<string> Lists
    {
        get
        {
            if (_profiles != null)
                return new ObservableCollection<string>(_profiles.Select(p => p.Name));
            else
                return null;
        }
    }

Question

Is there a better way? Is there a pattern/method that I haven't yet discovered?

Edit

The root of the problem appears when doing non-UI code, and you cannot rely on the NotifyPropertyChanged to do some thread-synchronization stuff. -- The method Initialize has to be awaited and ctors cannot be async, so essentialy this is pattern is useless.

    public MyClass()
    {
        Initialize();
    }

    public async void Initialize()
    {
        _profiles = await Profile.GetAllProfiles();
    }

    private ObservableCollection<Profile> _profiles;
    public ObservableCollection<string> Lists
    {
        get
        {
            return _profiles; // this will always be null
        }
    }

回答1:

Properties can't be async so this solution will not work as you mentioned. Task.Result waits for the task to complete, but this is blocking your UI thread where the I/O operation's async callback returns, so you are deadlocking your application, since the callback is never called. Your solution really is the best way. It could be improved though.

  • You should make the _profiles field an ObservableCollection, so you would not need to convert the List to the OC every time the list is accessed.
  • Since you are performing an I/O operation that can take arbitrary amount of time - you should enable some sort of a progress indicator while it is in progress.
  • In some cases you might want the Lists property to be lazier and only call the Init method the first time it is accessed.