2

Windows 8 アプリを作成していますが、非同期呼び出しで問題が発生しています。これには2つの結果があると思うので、できるだけ多くの詳細を提供しようとします:

  • 非同期呼び出しに関しては、私は完全に間違ったことをしています
  • または、私はそれを間違っていますが、そもそもそこにあるべきではないこの問題を私に突きつける間違ったアーキテクチャかもしれません。

私は Windows Azure と MVVM の初心者ですが、状況は次のとおりです...</p>

アプリは現在 Windows 8 用に構築されていますが、他のプラットフォームも使用できるようにしたいので、最初に行ったことは、Windows Azure Web サイトに発行される WebAPI プロジェクトを作成することです。そうすれば、JSON を使用してデータを転送でき、WebAPI コントローラーは、Window Azure Table Storage との間のデータ要求を処理するリポジトリに接続します。2 番目の部分は、Azure Web サイトからデータを要求する MVVM Light Windows 8 アプリです。

それでは、WebAPI プロジェクトを詳しく見てみましょう。ここでは、まずカテゴリ モデルを用意します。

public class Category : TableServiceEntity
{
    [Required]
    public string Name { get; set; }

    public string Description { get; set; }

    public string Parent { get; set; }
}

カテゴリ モデルには名前と説明が含まれているだけです (id は TableServiceEntity の RowKey です)。また、カテゴリがネストされている場合は、文字列参照が親カテゴリに追加されます。最初の疑問が生じます: 親は文字列ではなくカテゴリの型である必要があり、バックエンド側のカテゴリ モデルには子カテゴリのコレクションが必要ですか??

次に、リポジトリを定義する IRepository インターフェイスを用意しました。(進行中の作業 ;-)) また、仕様パターンを使用してクエリ範囲を渡します。ブラウザーを使用してテストし、http: //homebudgettracker.azurewebsites.net/api/categoriesを参照できるため、これはすべて機能しています。

public interface IRepository<T> where T : TableServiceEntity
{
    void Add(T item);
    void Delete(T item);
    void Update(T item);
    IEnumerable<T> Find(params Specification<T>[] specifications);
    IEnumerable<T> RetrieveAll();
    void SaveChanges();
}

リポジトリが明確になったので、コントローラーを見てみましょう。IRepository リポジトリを含む ApiController である CategoriesController があります。(Ninject で注入されますが、ここでは関係ありません)

public class CategoriesController : ApiController
{
    static IRepository<Category> _repository;

    public CategoriesController(IRepository<Category> repository)
    {
        if (repository == null)
        {
            throw new ArgumentNullException("repository");
        }

        _repository = repository;    
        }

コントローラーには、次のようないくつかのメソッドが含まれています。

public Category GetCategoryById(string id)
{    
    IEnumerable<Category> categoryResults =_repository.Find(new ByRowKeySpecification(id));

    if(categoryResults == null)
    {
        throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound));
    }

    if (categoryResults.First<Category>() == null)
    {
        throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound));
    }

    return categoryResults.First<Category>();
}

ここまでで、バックエンドを見てきました。ここで実際の問題に移りましょう。MvvmLight クライアントと、WebAPI コントローラーへの非同期 http 要求です。

クライアント側のプロジェクトには、カテゴリ モデルもあります。

public class Category
{
    [JsonProperty("PartitionKey")]
    public string PartitionKey { get; set; }

    [JsonProperty("RowKey")]
    public string RowKey { get; set; }

    [JsonProperty("Name")]
    public string Name { get; set; }

    [JsonProperty("Description")]
    public string Description { get; set; }

    [JsonProperty("Timestamp")]
    public string Timestamp { get; set; }

    [JsonProperty("Parent")]
    public string ParentRowKey { get; set; }

    public ObservableCollection<Category> Children { get; set; }
}

PartitionKey と RowKey のプロパティは気にしないでください。どの Azure テーブル サービス エンティティ プロパティが存在するかはアプリケーションには関係ないため、パーティション キーは除外する必要があります。RowKey は、実際には Id に名前を変更できます。しかし、ここでは実際には関係ありません。

メイン ビューの ViewModel は次のようになります。

public class MainViewModel : CategoryBasedViewModel
{
    /// <summary>
    /// Initializes a new instance of the MainViewModel class.
    /// </summary>
    public MainViewModel(IBudgetTrackerDataService budgetTrackerDataService)
: base(budgetTrackerDataService)
    {
        PageTitle = "Home budget tracker";
    }
}

これは、Category Observable コレクションを含むページのロジックを共有するために作成した ViewModel から拡張されています。この ViewModel の重要事項:

  • 高レベルのデータ サービスである ViewModel に挿入される IBudgetTrackerDataService
  • カテゴリ ViewModel でラップされたカテゴリのコレクションを含む ObservableCollection
  • バインディング用のいくつかのプロパティ (fe: ビューで ProgressRing を処理するための IsLoadingCategories)
  • 非同期呼び出しが完了した後に IBudgetTrackerDataService によって呼び出される getCategoriesCompleted コールバック メソッド

したがって、コードは次のとおりです。

public abstract class CategoryBasedViewModel : TitledPageViewModel
{
    private IBudgetTrackerDataService _dataService;

    private ObservableCollection<CategoryViewModel> _categoryCollection;

    private Boolean isLoadingCategories;

    public const string CategoryCollectionPropertyName = "CategoryCollection";

    public const string IsLoadingCategoriesPropertyName = "IsLoadingCategories";

    public Boolean IsLoadingCategories
    {
        get
        {
            return isLoadingCategories;
        }
        set
        {
            if (isLoadingCategories != value)
            {
                isLoadingCategories = value;
                RaisePropertyChanged(IsLoadingCategoriesPropertyName);
            }
        }
    }

    public ObservableCollection<CategoryViewModel> CategoryCollection
    {
        get
        {
            return _categoryCollection;
        }
        set
        {
            _categoryCollection = value;
            RaisePropertyChanged(CategoryCollectionPropertyName);
        }
    }

    public CategoryBasedViewModel(IBudgetTrackerDataService budgetTrackerDataService)
    {
        wireDataService(budgetTrackerDataService);
    }

    public CategoryBasedViewModel(IBudgetTrackerDataService budgetTrackerDataService, string pageTitle)
    {
        PageTitle = pageTitle;
        wireDataService(budgetTrackerDataService); 
    }

    private void wireDataService(IBudgetTrackerDataService budgetTrackerDataService)
    {
        _dataService = budgetTrackerDataService;
        CategoryCollection = new ObservableCollection<CategoryViewModel>();
        IsLoadingCategories = true;
        _dataService.GetCategoriesAsync(GetCategoriesCompleted);
    }

    private void GetCategoriesCompleted(IList<Category> result, Exception error)
    {
        if (error != null)
        {
            throw new Exception(error.Message, error);
        }

        if (result == null)
        {
            throw new Exception("No categories found");
        }

        IsLoadingCategories = false;

        CategoryCollection.Clear();

        foreach (Category category in result)
        {
            CategoryCollection.Add(new CategoryViewModel(category, _dataService));
            // Added the dataService as a parameter because the CategoryViewModel will handle the search for Parent Category and Children catagories
        }
    }
}

これはすべて機能していますが、親子関係をカテゴリで機能させたいと思います。このため、CategoryViewModel にロジックを追加して、構築時に子カテゴリをフェッチするようにしました…</p>

public CategoryViewModel(Category categoryModel, IBudgetTrackerDataService
budgetTrackerDataService)
{
    _category = categoryModel;
    _dataService = budgetTrackerDataService;

    // Retrieve all the child categories for this category
    _dataService.GetCategoriesByParentAsync(_category.RowKey,
GetCategoriesByParentCompleted);
}

したがって、CategoryBasedViewModel の構築は、カテゴリをフェッチし、コールバック メソッド GetCategoriesCompleted を呼び出します。

_dataService.GetCategoriesAsync(GetCategoriesCompleted);

そのコールバック メソッドは、CategoryViewModel のコンストラクターも呼び出しています。そこでは、別の非同期メソッドを使用して、カテゴリの子をフェッチします。

public CategoryViewModel(Category categoryModel, IBudgetTrackerDataService
budgetTrackerDataService)
{
    _category = categoryModel;
    _dataService = budgetTrackerDataService;

    // Retrieve all the child categories for this category
    _dataService.GetCategoriesByParentAsync(_category.RowKey,
GetCategoriesByParentCompleted);
}

そして、私の問題があります!GetCategoriesByParentAsync は、他の非同期呼び出し内で発生する非同期呼び出しであり、コードは呼び出しから抜け出し、何もしません。

データ サービスは次のインターフェイスを実装します。

public interface IBudgetTrackerDataService
{
    void GetCategoriesAsync(Action<IList<Category>, Exception> callback);

    void GetCategoriesByParentAsync(string parent, Action<IList<Category>,
Exception> callback);
}

非同期メソッドには、次のコードが含まれています。

public async void GetCategoriesAsync(Action<IList<Category>, Exception> callback)
{
    // Let the HTTP client request the data
    IEnumerable<Category> categoryEnumerable = await _client.GetAllCategories();

    // Invoke the callback function passed to this operation
    callback(categoryEnumerable.ToList<Category>(), null);
}

public async void GetCategoriesByParentAsync(string parent, Action<IList<Category>,
Exception> callback)
{
    // Let the HTTP client request the data
    IEnumerable<Category> categoryEnumerable = await
_client.GetCategoriesWithParent(parent);

    // Invoke the callback function passed to this operation
    callback(categoryEnumerable.ToList<Category>(), null);
}

簡単に言えば:

  • 呼び出しをネストすると、これらの呼び出しが失敗するのはなぜですか?
  • 第二に、私は愚かで、檻の親子関係を別の方法で処理する必要がありますか?
4

1 に答える 1

4

今のところ、親子関係の質問を回避し、async問題に対処するだけです。

まず、コードの一般的なガイドラインがいくつかあります。詳細については、私の/イントロ ブログ投稿asyncで説明しています。asyncawait

  • 避けるasync void(返すTask、またはTask<T>代わりに)。
  • 該当する場合に使用ConfigureAwait(false)します。

他の人が取った委任アプローチを見てきましたcallbackが、それがどこから来ているのかわかりません。それはうまく機能せずasync、コードを複雑にするだけです, IMO. このTask<T>型は、 と組み合わせた結果値を表すように設計されており、Exceptionとシームレスに動作しますawait

まず、データ サービス:

public interface IBudgetTrackerDataService
{
  Task<IList<Category>> GetCategoriesAsync();
  Task<IList<Category>> GetCategoriesByParentAsync(string parent);
}

public async Task<IList<Category>> GetCategoriesAsync()
{
  // Let the HTTP client request the data
  IEnumerable<Category> categoryEnumerable = await _client.GetAllCategories().ConfigureAwait(false);
  return categoryEnumerable.ToList();
}

public async Task<IList<Category>> GetCategoriesByParentAsync(string parent)
{
  // Let the HTTP client request the data
  IEnumerable<Category> categoryEnumerable = await _client.GetCategoriesWithParent(parent).ConfigureAwait(false);
  return categoryEnumerable.ToList();
}

または、実際に必要ない場合はさらに良いIList<T>

public interface IBudgetTrackerDataService
{
  Task<IEnumerable<Category>> GetCategoriesAsync();
  Task<IEnumerable<Category>> GetCategoriesByParentAsync(string parent);
}

public Task<IEnumerable<Category>> GetCategoriesAsync()
{
  // Let the HTTP client request the data
  return _client.GetAllCategories();
}

public Task<IEnumerable<Category>> GetCategoriesByParentAsync(string parent)
{
  // Let the HTTP client request the data
  return _client.GetCategoriesWithParent(parent);
}

(その時点で、データ サービスが提供している目的を疑問視するかもしれません)。


asyncMVVM の問題に移ります:asyncコンストラクターでは特にうまく機能しません。これについて詳しく説明するブログ投稿を数週間以内に公開しますが、要点は次のとおりです。

私の個人的な好みは、非同期ファクトリ メソッド (例: public static async Task<MyType> CreateAsync()) を使用することですが、特に VM に DI/IoC を使用している場合は、これが常に可能であるとは限りません。

この場合、非同期初期化を必要とする型のプロパティを公開するのが好きです (実際にはIAsyncInitializationインターフェイスを使用しますが、コードでは規約も同様に機能します) public Task Initialized { get; }

このプロパティは、次のようにコンストラクターで 1 回だけ設定されます。

public CategoryViewModel(Category categoryModel, IBudgetTrackerDataService budgetTrackerDataService)
{
  _category = categoryModel;
  _dataService = budgetTrackerDataService;

  // Retrieve all the child categories for this category
  Initialized = InitializeAsync();
}

private async Task InitializeAsync()
{
  var categories = await _dataService.GetCategoriesByParentAsync(_category.RowKey);
  ...
}

次に、「子」VM が初期化されるまで「親」VM を待機させるオプションがあります。これがあなたが望んでいるものかどうかは明らかではありませんが、すべての子 VM が読み込まIsLoadingCategoriesれるまで、あなたがそうしたいと思っていると仮定します。true

public CategoryBasedViewModel(IBudgetTrackerDataService budgetTrackerDataService)
{
  _dataService = budgetTrackerDataService;
  CategoryCollection = new ObservableCollection<CategoryViewModel>();
  IsLoadingCategories = true;
  Initialized = InitializeAsync();
  NotifyOnInitializationErrorAsync();
}

private async Task InitializeAsync()
{
  var categories = await _dataService.GetCategoriesAsync();
  CategoryCollection.Clear();
  foreach (var category in categories)
  {
    CategoryCollection.Add(new CategoryViewModel(category, _dataService));
  }

  // Wait until all CategoryViewModels have completed initializing.
  await Task.WhenAll(CategoryCollection.Select(category => category.Initialized));

  IsLoadingCategories = false;
}

private async Task NotifyOnInitializationErrorAsync()
{
  try
  {
    await Initialized;
  }
  catch
  {
    NotifyPropertyChanged("InitializationError");
    throw;
  }
}

public string InitializationError { get { return Initialized.Exception.InnerException.Message; } }

と を追加してInitializationErrorNotifyOnInitializationErrorAsync初期化中に発生する可能性のあるエラーを表示する 1 つの方法を示しました。Taskは を実装していないため、INotifyPropertyChanged初期化が失敗した場合の自動通知がないため、明示的に表示する必要があります。

于 2013-01-02T03:49:17.547 に答える