2

我正在构建一个 Windows 8 应用程序,但我遇到了异步调用问题。我将尝试提供尽可能多的细节,因为我认为我有两个结果:

  • 当涉及到异步调用时,我做错了
  • 或者我做错了,但也可能是错误的架构让我遇到了这个问题,而这个问题本来就不应该存在。

我是 Windows Azure 和 MVVM 的新手,但情况就是这样……</p>

该应用程序现在是为 Windows 8 构建的,但我也希望能够使用其他平台,所以我首先要做的是创建一个发布到 Windows Azure 网站的 WebAPI 项目。这样,我可以使用 JSON 传输数据,并且 WebAPI 控制器连接到一个存储库,该存储库正在处理与 Window Azure 表存储之间的数据请求。第二部分是一个 MVVM Light Windows 8 应用程序,它从 Azure 网站请求数据。

因此,让我们更详细地看一下 WebAPI 项目。在这里,我有一个类别模型开始。

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

    public string Description { get; set; }

    public string Parent { get; set; }
}

类别模型只包含一个名称和描述(id 是 TableServiceEntity 的 RowKey)。如果类别是嵌套的,还会将字符串引用添加到父类别。出现第一个问题:Parent 是否应该是 Category 类型而不是 string,后端的 Category 模型是否应该有一组子类别?

然后我有我的 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();
}

现在存储库已经清楚了,让我们看一下控制器。我有一个 CategoriesController,它只是一个包含 IRepository 存储库的 ApiController。(用 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";
    }
}

它从我创建的 ViewModel 扩展而来,用于共享包含 Category Observable 集合的页面的逻辑。此 ViewModel 中的重要内容:

  • 一个 IbudgetTrackerDataService 注入到 ViewModel 中,这是一个高级数据服务
  • 一个 ObservableCollection 包含 category-ViewModel-wrapped 类别的集合
  • 一些用于绑定的属性(fe: IsLoadingCategories 来处理视图上的 ProgressRing)
  • 异步调用完成后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首先,我在async/ awaitintro 博客文章中详细解释了一些关于代码的一般准则:

  • 避免async void(返回TaskTask<T>代替)。
  • 适用时使用ConfigureAwait(false)

我见过callback其他人采取的代表方法,但我不确定它来自哪里。它不能很好地工作async,只会使代码复杂化,IMO。该Task<T>类型旨在表示与 an 耦合的结果值,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);
}

(此时,您可能会质疑您的数据服务的目的是什么)。


继续讨论 MVVMasync问题:async在构造函数中表现不佳。几周后我会发表一篇博文,将详细讨论这个问题,但要点如下:

我个人的偏好是使用异步工厂方法(例如,public static async Task<MyType> CreateAsync()),但这并不总是可行的,尤其是当您为 VM 使用 DI/IoC 时。

在这种情况下,我喜欢在需要异步初始化的类型上公开一个属性(实际上,我使用了一个IAsyncInitialization接口,但是对于您的代码,约定也可以正常工作)public Task Initialized { get; }

此属性在构造函数中只设置一次,如下所示:

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);
  ...
}

然后,您可以选择让您的“父”虚拟机等待其“子”虚拟机初始化。目前尚不清楚这是否是您想要的,但我假设您希望IsLoadingCategoriestrue所有子虚拟机都加载之前成为:

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; } }

我添加了InitializationErrorandNotifyOnInitializationErrorAsync来演示一种方法来显示初始化期间可能发生的任何错误。由于Task未实现INotifyPropertyChanged,因此如果/当初始化失败时没有自动通知,因此您必须明确显示它。

于 2013-01-02T03:49:17.547 回答