web-dev-qa-db-ja.com

ViewModelに非同期で(asyncとawaitを使用して)データをロードしても、データバインディングが機能しない

ビューモデルがすでに定義されているデフォルトのテンプレートを使用して電話アプリを起動しました。 MainViewModelのLoadData()メソッドを変更して、odataサービスを非同期的に呼び出すようにしました。ただし、データバインディングでは機能しません。呼び出しが正常に返されることを確認しましたが、結果が表示されません。

LongListSelectorのitemsソースは、ビューモデルのItemsプロパティにバインドされています。

<phone:LongListSelector ItemsSource="{Binding Items}" x:Name="MainLongListSelector" Margin="0,0,-12,0" SelectionChanged="MainLongListSelector_SelectionChanged">
                <phone:LongListSelector.ItemTemplate>
                    <DataTemplate>
                      <StackPanel Margin="0,0,0,17">
                            <TextBlock Text="{Binding UnReadCount}" TextWrapping="Wrap" Style="{StaticResource PhoneTextExtraLargeStyle}"/>
                            <TextBlock Text="{Binding description}" TextWrapping="Wrap" Margin="12,-6,12,0" Style="{StaticResource PhoneTextSubtleStyle}"/>
                      </StackPanel>
                    </DataTemplate>
                </phone:LongListSelector.ItemTemplate>
            </phone:LongListSelector>

ビューモデルへの私の変更は次のとおりです(非同期に注意して使用を待つ):

public void LoadData()
    {
        FetchTileViewItems();        
    }

    private async void FetchTileViewItems()
    {
        var ret = await I2ADataServiceHelper.GetTileViewItemsAsync();
        this.Items = new ObservableCollection<TileViewItem>(ret);
        this.IsDataLoaded = true;
    }

そして、前と同じように、ページのNavigatedToイベントでLoadData()メソッドを呼び出しています。

protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            if (!App.ViewModel.IsDataLoaded)
            {
                App.ViewModel.LoadData();
                pr1.IsVisible = false;
            }
        }

実行を押しても何も表示されません...何かが足りませんか?どんなポインタでも大歓迎です。

15
user2137225

わかりました。簡単な答えは、INotifyPropertyChangedおよび/またはItemsセッターでIsDataLoaded通知が欠落している可能性があるということです。

長い答えは少しかかります。 :)

まず、async voidを避ける必要があります。理由については、私の 非同期プログラミングのベストプラクティス の記事で詳しく説明しています。この場合、エラー処理を検討してください。あなたの幸せなケースが「呼び出しが正常に戻った」ときであるのは素晴らしいことですが、悲しいケースはあなたのプログラムを破壊するでしょう。

それでは、すべてを可能な限りasync Taskとして書き直し、 *Async規則に従ってください 作業中は次のようにします。

public async Task LoadDataAsync()
{
    await FetchTileViewItemsAsync();
}

private async Task FetchTileViewItemsAsync()
{
    var ret = await I2ADataServiceHelper.GetTileViewItemsAsync();
    this.Items = new ObservableCollection<TileViewItem>(ret);
    this.IsDataLoaded = true;
}

protected override async void OnNavigatedTo(NavigationEventArgs e)
{
    if (!App.ViewModel.IsDataLoaded)
    {
        await App.ViewModel.LoadDataAsync();
    }
}

これは、asyncコードを書くためのより自然な方法です。

次に、そのエラー状況を修正しましょう。あなたtry/catchOnNavigatedToで行うことができます

protected override async void OnNavigatedTo(NavigationEventArgs e)
{
    try
    {
        if (!App.ViewModel.IsDataLoaded)
        {
            await App.ViewModel.LoadDataAsync();
        }
    }
    catch (Exception ex)
    {
        ...
    }
}

しかし、私は実際には、エラー処理のためにViewModel中心のデータバインディングに適したシステムに傾倒しています。このように、「切断」はアプリケーションにとって完全に自然な状態です。エラーメッセージを表示するだけでも、アプリケーションは、たまに接続されるシステム(電話など)用に設計されることになります。また、結果のコードはよりテスト可能です。

このアプローチについては、いくつかのブログ投稿で説明しています。asyncコンストラクターに関する投稿で 非同期初期化パターン を取り上げ、 特にデータバインディング = asyncプロパティに関する私の投稿。 TaskCompletionNotifier というヘルパークラスを作成しました。これにより、データバインディングでTaskを使用できるようになります。

これらのデザインを配置すると、ViewModelコードは次のようになります。

public sealed class MyViewModel : INotifyPropertyChanged
{
    public ObservableCollection<TileViewItem> Items
    {
      get { return _items; }
      private set { _items = value; RaisePropertyChanged(); }
    }

    public ITaskCompletionNotifier Initialization { get; private set; }

    public MyViewModel()
    {
        Initialization = TaskCompletionNotifierFactory.Create(InitializeAsync());
    }

    private async Task InitializeAsync()
    {
        var ret = await I2ADataServiceHelper.GetTileViewItemsAsync();
        this.Items = new ObservableCollection<TileViewItem>(ret);
    }
}

(これは、コンストラクターでデータのロードを開始することを前提としています。)

次に、Itemsに直接バインドできます。また、幸せな場合はInitialization.IsSuccessfullyCompletedにバインドでき、悲しい場合はInitialization.IsFaultedInitialization.ErrorMessageにバインドできます。

34
Stephen Cleary