Как я могу привязать мой интерфейс WP7 к данным, которые я извлекаю асинхронно?

#c# #windows-phone-7 #asynchronous #httpwebrequest

#c# #windows-phone-7 #асинхронный #httpwebrequest

Вопрос:

Я работаю над приложением для Windows Phone 7, которое позволяет пользователю просматривать статистику каждого из своих «сайтов» в их Chargify.com учетная запись.

Я просматривал обучающее видео с множественным обзором, которое помогло мне пройти большую часть пути, однако мои данные поступают из сложного источника, и у них был жестко запрограммированный список.

Итак, вот настройка:

Модель:

  public SiteStats
 {
     public string seller_name { get; set;}
     public static GetSiteStatistics(string subdomain, string apiKey)
     {
        SiteStats retVal = null;
        HttpWebRequest request = WebRequest.Create(string.Format("https://{0}.chargify.com/stats.json", subdomain)) as HttpWebRequest;
        NetworkCredential credentials = new NetworkCredential(apiKey, "X");
        request.Credentials = credentials;
        request.Method = "GET";
        request.Accept = "application/json";
        request.BeginGetResponse(result =>
        {
            using (var response = request.EndGetResponse(result))
            {
                using (StreamReader reader = new StreamReader(response.GetResponseStream()))
                {
                    string stats = reader.ReadToEnd();
                    retVal = Json.Deserialize<SiteStats>(stats);
                }
            }
        }, request);
        return retVal;
     }
 }
  

ViewModel:

  public class SiteDetailViewModel : ViewModelBase
 {
    private SiteStats _siteStats;
    public SiteDetailViewModel(string subdomain) : this()
    {
       this._siteStats = SiteStats.GetSiteStatistics(subdomain, "apiKeyHere");
    }
    public SiteDetailViewModel : base() { ViewName = "site details"; }
    public SiteStats SiteStats
    {
      get { return _siteStats; }
      set {
        if (_siteStats != value) {
          _siteStats = value;
          OnPropertyChanged("SiteStats");
        }
     }
 }
  

Представление:

  public partial class SiteDetailView : PhoneApplicationPage
 {
   private SiteDetailViewModel _viewModel;

   public SiteDetailView()
   {
      InitializeComponent();
      Loaded  = new RoutedEventHandler(SiteDetailView_Loaded);
   }
   void SiteDetailView_Loaded(object sender, RoutedEventArgs e)
   {
      string subdomain = NavigationContext.QueryString["subdomain"];
      _viewModel = new SiteDetailViewModel(subdomain);
      this.DataContext = _viewModel;
   }
 }
  

Проблема в том, что когда я вызываю это.DataContext — член _viewModel еще не имеет своих данных. Итак, view привязывает данные, но значение пустое.

Есть предложения? Все работает нормально, за исключением того, что представление не заполняет привязанные элементы управления к данным..

— Kori

Комментарии:

1. Как выглядит ваше представление, в котором вы выполняете привязку? Просто скопируйте один из текстовых блоков / etc. Я предполагаю, что вы привязываете onetime и вам нужно использовать режим привязки oneway.

2. Это выглядит следующим образом: <Высота текстового блока = «30» Горизонтальное выравнивание =»Левое» поле = «351,23,0,0» Имя = «sellerNameTbl» Текст =»{Путь привязки=SiteStats.seller_name}» Вертикальное выравнивание =»Top» />

Ответ №1:

_viewModel должно быть ObservableCollection<> для статистики, и вы привязываете свой пользовательский интерфейс к этой коллекции. Всякий раз, когда элементы добавляются в коллекцию или удаляются из нее, пользовательский интерфейс обновляется автоматически (поскольку он отправляет OnPropertyChanged событие)

Комментарии:

1. Это только половина решения. Коллекция также должна быть потокобезопасной или заполняться из потока пользовательского интерфейса. И его SiteStats — это не коллекция, а отдельный класс с одним строковым свойством.

2. Есть и другие свойства, но в целом это довольно простой объект, который я получаю обратно — я просто не хотел вводить все это. / ленивый

Ответ №2:

Ваша проблема не в WPF, а в GetSiteStatistics. Поскольку вы получаете результат асинхронно, ваш метод почти всегда возвращает null, если только случайно BeginGetResponse не выполняется до возврата метода GetSiteStatistics. Это привело бы к сбою в любом приложении.

Вы могли бы заставить GetSiteStatistics всегда создавать и возвращать объект и заполнять его только в BeginGetResponse . Но тогда вы должны убедиться, что все это потокобезопасно.

Ответ №3:

Хорошо, я думаю, с этим разобрался..

Это было там все время, я просто пропустил это.

1) вы получаете данные асинхронно. «Запрос.Запуск ответа (результат => …. );» произойдет когда-нибудь в будущем, но вы возвращаете свой результат до того, как это произойдет.. код продолжает перемещаться, он не ждет вашего результата. Вот что вы хотите сделать:

 public class SiteStats
{
  public string seller_name { get; set;}
  public static void GetSiteStatistics(string subdomain, string apiKey, Action<SiteStats> callback)
  {
    SiteStats retVal = null;
    HttpWebRequest request = WebRequest.Create(string.Format("https://{0}.chargify.com/stats.json", subdomain)) as HttpWebRequest;
    NetworkCredential credentials = new NetworkCredential(apiKey, "X");
    request.Credentials = credentials;
    request.Method = "GET";
    request.Accept = "application/json";
    request.BeginGetResponse(result =>
    {
        using (var response = request.EndGetResponse(result))
        {
            using (StreamReader reader = new StreamReader(response.GetResponseStream()))
            {
                string stats = reader.ReadToEnd();
                retVal = Json.Deserialize<SiteStats>(stats);
                callback(retVal);
            }
        }
    }, request);
    //return retVal; // you can't return here
  }
}
  

Соответствующий код ViewModel будет выглядеть примерно так:

 public SiteDetailViewModel(string subdomain) : this()
{
   SiteStats.GetSiteStatistics(subdomain, "apiKeyHere", (result)=> {
     // Note you may need to wrap this in a Dispatcher call 
     // as you may be on the wrong thread to update the UI 
     // if that happens you'll get a cross thread access 
     // you will have to expose the dispatcher through some 
     // other mechanism. One way to do that would be a static
     // on your application class which we'll emulate and 
     // I'll give you the code in a sec
     myRootNamespace.App.Dispatcher.BeginInvoke(()=>this._siteStats = results);
   });
}
  

Вот изменения, которые вам нужно внести в класс Application (я не уверен, насколько это потокобезопасно, и я бы действительно рекомендовал вам использовать что-то вроде DispatcherHelper от MVVMLight.

 public partial class App : Application
{

    public static Dispatcher Dispatcher { get; private set; } // Add this line!!

    // More code follows we're skipping it

    private void Application_Startup(object sender, StartupEventArgs e)
    {
        this.RootVisual = new MainPage(); 
        Dispatcher = this.RootVisual.Dispatcher; // only add this line!!
    }

    private void Application_Exit(object sender, EventArgs e)
    {
        // Do this to clean up
        Dispatcher = null;
    }
}