Статьи

Образец Windows Azure Mobile Services «Аренда дома», часть 2: пользовательский интерфейс и данные

В предыдущей части мы увидели общий интерфейс приложения. Теперь мы рассмотрим, как этот интерфейс был реализован на всех четырех платформах. Если вы ищете быстрый старт или документацию по мобильным сервисам, вам следует обратиться в  Центр разработчиков мобильных приложений Windows Azure .

Android
Модель класса для списков квартир на Android выглядит следующим образом:

public class Apartment implements Serializable {
  private int id; 
  private String address;
  private boolean published;
  private int bedrooms;
  private double latitude;
  private double longitude;
  private String username;
  //Getters and setters omitted for brevity
}

Обратите внимание, что класс не  должен  быть Java-сериализуемым, но его типы полей ограничены тем, что в настоящее время поддерживает Mobile Services. Android SDK использует  библиотеку gson  (быстрый и расширяемый сериализатор JSON от Google) для сериализации объектов в сети. Причина, по которой мой класс реализует рыночный интерфейс Serializable, заключается в том, что его можно передавать через действия.

Основное действие на Android загружает простой макет, который состоит из  ListViewпривязанного к пользовательскому адаптеру (производного от  ArrayAdapter<Apartment>). Он отображает три значения  TextViewдля каждого списка квартир: адрес квартиры, количество спален и пользователя, который представил этот список квартир.

public class ApartmentAdapter extends ArrayAdapter<Apartment> {
  public ApartmentAdapter(Context context,
                          List<Apartment> apartments) {
    super(context, R.layout.apartment_row, apartments);
  }
  
  @Override
  public View getView(int position, View row, ViewGroup parent) {
    Apartment apartment = getItem(position);
    if (row == null) {
      LayoutInflater inflater = LayoutInflater.from(getContext());
      row = inflater.inflate(R.layout.apartment_row, null);
    }
    TextView address = (TextView)row.findViewById(R.id.txtAddress);
    TextView username = (TextView)row.findViewById(R.id.txtSecondary);
    TextView bedrooms = (TextView)row.findViewById(R.id.txtBedrooms);
    address.setText(apartment.getAddress());
    username.setText("added by " + apartment.getUserName()));
    bedrooms.setText(Integer.toString(apartment.getBedrooms()));
    return row;
  } 
}

Когда действие инициализируется, оно извлекает список списков квартир из серверной части Mobile Services и связывает его с пользовательским интерфейсом с помощью специального адаптера:

mobileService = new MobileServiceClient(
            MOBILESERVICE_URL, MOBILESERVICE_APIKEY, this);
apartmentTable = mobileService.getTable("apartment", Apartment.class);
apartmentTable
  .where()
  .field("published").eq(true).and()
  .field("bedrooms").gt(1)
  .orderBy("bedrooms", QueryOrder.Descending)
  .execute(new TableQueryCallback<Apartment>() {
    public void onCompleted(List<Apartment> items, int count,
                            Exception exception,
                            ServiceFilterResponse response) {
      if (exception != null) {
        displayError(exception);
      } else {
        ApartmentAdapter aa = new ApartmentAdapter(this, items);
        listApartments.setAdapter(aa);
      }
    }
  });

Чтобы добавить новый список квартир, пользователь нажимает на элемент меню / панели действий «Добавить» и получает диалоговое окно, которое собирает информацию о листинге и передает ее в мобильные сервисы:

//Dialog view setup omitted for brevity
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Add New Apartment");
builder.setView(innerLayout);
builder.setPositiveButton("Submit", new OnClickListener() {
  public void onClick(DialogInterface dialog, int which) {
    Apartment apartment = new Apartment();
    apartment.setAddress(editAddress.getText().toString());
    apartment.setBedrooms((Integer)spinBedrooms.getSelectedItem());
    apartment.setPublished(true);
    apartmentTable.insert(apartment,
      new TableOperationCallback() /* omitted for brevity */);
  }
});
builder.setNegativeButton("Cancel", null);
builder.create().show();

Наконец, когда активируется карта, она отображает списки квартир с помощью простого наложения карты поверх Google  MapView (для использования Google Maps в вашем приложении требуется ключ API, который вы получаете  онлайн ). Наложение основывается на координатах, предоставленных сервером при вставке новых списков квартир (позже мы увидим, как это происходит). При касании элемента наложения наложение отображает простой диалог с деталями списка.

public class ApartmentOverlay extends ItemizedOverlay<OverlayItem> {
  private List<OverlayItem> items = new ArrayList<OverlayItem>();
  private List<Apartment> apartments = new ArrayList<Apartment>();
  private Context context;

  public void addApartment(Apartment apartment) {
    Location location = apartment.getLocation();
    items.add(new OverlayItem(new GeoPoint(
      (int) (location.getLatitude() * 1E6),
      (int) (location.getLongitude() * 1E6)),
      "Apartment",
      apartment.getAddress()));
    apartments.add(apartment);
    populate();
  }

  public ApartmentOverlay(Context ctx, Drawable defaultMarker) {
    super(boundCenterBottom(defaultMarker));
    context = ctx;
    populate();
  }

  @Override
  protected OverlayItem createItem(int i) {
    return items.get(i);
  }

  @Override
  public int size() {
    return items.size();
  }

  @Override
  protected boolean onTap(int index) {
    Apartment apartment = apartments.get(index);
    AlertDialog.Builder builder = new AlertDialog.Builder(context);
    builder.setTitle("Apartment");
    builder.setMessage("Address: " + apartment.getAddress() + "\n" +
                       apartment.getBedrooms() + " bedrooms");
    builder.create().show();
    return super.onTap(index);
  }
}

iOS
Платформа мобильных сервисов в iOS не использует статические типы для передачи информации по сети. Вместо этого он использует  NSDictionary класс, который содержит коллекцию пар ключ-значение. Хотя моя реализация может предоставить статический  Apartment класс, который будет «сериализован» в представление и из NSDictionary представления, я решил использовать его  NSDictionary повсеместно. Если бы модель была более сложной, я мог бы рассмотреть подход статического типа.

Основной контроллер представления на iOS содержит,  UITableView который использует стиль ячейки представления таблицы субтитров. Когда контроллер представления инициализирован, он извлекает список квартир списков из внутреннего интерфейса Mobile Services, и предоставляет его  UITableView в  UITableViewDelegate«х  numberOfSectionsInTableView:, tableView:numberOfRowsInSection:и  tableView:cellForRowAtIndexPath: методе.

- (void)viewDidLoad {
  [super viewDidLoad];
  self.client = [MSClient clientWithApplicationURLString:kMobileAppURL
                                      withApplicationKey:kMobileAppKey];
  self.table = [self.client getTable:@"apartment"];
  NSPredicate *predicate = [NSPredicate
                            predicateWithFormat:@"published == YES"];
  [self.table readWhere:predicate
             completion:^(NSArray *results, NSInteger totalCount, NSError *error) {
    self.items = [results mutableCopy];
  }];
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView
 numberOfRowsInSection:(NSInteger)section {
    return [self.items count];
}

- (UITableViewCell *)tableView:(UITableView *)tv
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  static NSString *CellIdentifier = @"Cell";
  UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier:CellIdentifier
                              forIndexPath:indexPath];
  NSDictionary *apt = [self.items objectAtIndex:indexPath.row];
  cell.textLabel.text = apt[@"address"];
  cell.detailTextLabel.text = [NSString stringWithFormat:@"%d bedrooms",
                               [apt[@"bedrooms"] integerValue]];
  return cell;
}

Чтобы добавить список квартир, пользователь переходит к вторичному контроллеру представления, который использует  UITableView статические ячейки для сбора адреса квартиры и количества спален. Когда пользователь нажимает «Сохранить», вторичный контроллер представления создает новый  NSDictionary с подробными сведениями о квартире и предоставляет его своему делегату (который является контроллером домашнего просмотра), который, в свою очередь, вставляет список квартир в бэкэнд Mobile Services:

//In the secondary view controller:
- (IBAction)saveTapped:(id)sender {
  NSDictionary *apartment = @{
    @"address" : self.itemText.text,
    @"bedrooms" : @(self.bedrooms.selectedSegmentIndex+1),
    @"published" : @(YES)
  };
  if ([self.delegate respondsToSelector:@selector(saveApartment:)]) {
    [self.delegate performSelector:@selector(saveApartment:)
                        withObject:apartment];
  }
}

//In the primary view controller:
- (void)saveApartment:(NSDictionary *)apartment {
  [self.navigationController popViewControllerAnimated:YES];

  __weak HomeController *s = self;
  [self.table insert:item completion:^(NSDictionary *result, NSError *error) {
    //Error handling omitted for brevity
    NSUInteger index = [self.items count];
    [s.items addObject:result];
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index
                                                inSection:0];
    [s.tableView insertRowsAtIndexPaths:@[ indexPath ]
                       withRowAnimation:UITableViewRowAnimationTop];
  }];
}

Наконец, если пользователь переходит к контроллеру представления карты, он отображает списки квартир, используя ApartmentAnnotation класс, который реализует  MKAnnotation протокол — это указывает,  MKMapView где отображать квартиры на карте.

//The MapViewController's viewDidLoad method:
- (void)viewDidLoad {
  for (NSDictionary *apartment in self.apartments) {
    MKAnnotation *annotation = [[ApartmentAnnotation alloc]
                                initWithApartment:apartment];
    [self.mapView addAnnotation:annotation];
  }
}

//The ApartmentAnnotation class:
@interface ApartmentAnnotation : NSObject <MKAnnotation>

- (id)initWithApartment:(NSDictionary *)apartment;

@end

@implementation ApartmentAnnotation

- (id)initWithApartment:(NSDictionary *)apartment {
    if (self = [super init]) {
        self.apartment = apartment;
    }
    return self;
}

- (NSString *)title {
    return self.apartment[@"address"];
}

- (NSString *)subtitle {
    return [NSString stringWithFormat:@"%d bedrooms",
            [self.apartment[@"bedrooms"] integerValue]];
}

- (CLLocationCoordinate2D)coordinate {
    return CLLocationCoordinate2DMake(
      [self.apartment[@"latitude"] doubleValue],
      [self.apartment[@"longitude"] doubleValue]
    );
}

@end

Windows Phone 8
Windows Phone Azure Mobile Services SDK поддерживает типизированные данные, как и версия для Android. Этот Apartment класс очень похож на версию для Android, а   атрибуты [DataTable]/ [DataMember]помогают настроить сериализованный вывод JSON в соответствии с моделью бэкэнда.

[DataTable(Name = "apartment")]
public class Apartment
{
  public int Id { get; set; }

  [DataMember(Name = "address")]
  public string Address { get; set; }

  [DataMember(Name = "published")]
  public bool Published { get; set; }

  [DataMember(Name = "bedrooms")]
  public int Bedrooms { get; set; }

  [DataMember(Name = "latitude")]
  public double Latitude { get; set; }

  [DataMember(Name = "longitude")]
  public double Longitude { get; set; }

  [DataMember(Name = "username")]
  public string UserName { get; set; }
}

В пользовательском интерфейсе Windows Phone используется элемент управления Pivot, который позволяет перемещаться между списком квартир и отображать их на карте, а также на дополнительной странице, которая используется для добавления новых списков квартир. Список квартир представляет собой простой  ListBox элемент управления, который имеет шаблон данных с несколькими  TextBlockс. Элемент управления Pivot настроен следующим образом:

<phone:Pivot Title="RENT A HOME">
  <phone:PivotItem Header="apartments">
    <StackPanel>
      <ListBox x:Name="listApartments">
        <ListBox.ItemTemplate>
          <DataTemplate>
            <StackPanel Orientation="Vertical">
              ... three TextBox controls omitted for brevity ...
            </StackPanel>
          </DataTemplate>
        </ListBox.ItemTemplate>
      </ListBox>
    </StackPanel>
  </phone:PivotItem>
  <phone:PivotItem Header="map">
    <maps:Map x:Name="mapApartments" CartographicMode="Hybrid"
                                     LandmarksEnabled="True" />
  </phone:PivotItem>
  <phone:PivotItem Header="new">
    <StackPanel>
      ... standard UI for adding listings omitted for brevity ...
    </StackPanel>
  </phone:PivotItem>
</phone:Pivot>

Когда страница загружается, приложение извлекает списки квартир из серверной части мобильной службы и связывает полученный список с  ListBox. LINQ-подобный синтаксис очень удобен для выражения запросов, таких как получение только опубликованных списков квартир, а поддержка асинхронных методов в C # позволяет очень просто выполнять эту операцию асинхронно с помощью  await оператора:

var items = await MobileService.GetTable<Apartment>()
                               .Where(a => a.Published == true)
                               .ToListAsync();
listApartments.ItemsSource = items;

Приложение Windows Phone использует элемент управления Nokia Maps, который является рекомендуемым каркасом карт для Windows Phone 8 ( Microsoft.Phone.Maps пространство имен). Списки квартир отображаются в верхней части карты в виде простых оверлеев, которые при нажатии отображают сообщение с подробной информацией о квартире и увеличивают местоположение списка на карте:

mapApartments.Layers.Clear();
MapLayer layer = new MapLayer();
foreach (Apartment apartment in apartments)
{
  MapOverlay overlay = new MapOverlay();
  overlay.GeoCoordinate = new GeoCoordinate(
                  apartment.Latitude, apartment.Longitude);
  overlay.PositionOrigin = new Point(0, 0);
  Grid grid = new Grid
  {
    Height = 40,
    Width = 25,
    Background = new SolidColorBrush(Colors.Red)
  };
  TextBlock text = new TextBlock
  {
    Text = apartment.Bedrooms.ToString(),
    VerticalAlignment = VerticalAlignment.Center,
    HorizontalAlignment = HorizontalAlignment.Center
  };
  grid.Children.Add(text);
  overlay.Content = grid;
  grid.Tap += (s, e) =>
  {
    MessageBox.Show(
      "Address: " + apartment.Address + Environment.NewLine +
      apartment.Bedrooms + " bedrooms",
      "Apartment", MessageBoxButton.OK);
    mapApartments.SetView(overlay.GeoCoordinate, 15,
                          MapAnimationKind.Parabolic);
  };
  layer.Add(overlay);
}
mapApartments.Layers.Add(layer);

Windows 8
Реализация Windows 8 поразительно похожа на Windows Phone. Фактически, последний выпуск Windows Azure Mobile Services объединяет большую часть .NET-сред в единую переносимую библиотеку классов, причем в качестве отдельных вспомогательных сборок предоставляются только второстепенные детали (это стало возможным благодаря появлению долгожданной поддержки переносимой библиотеки классов для  HttpClient класса. ). Это означает, что модель нашего приложения также может быть помещена в переносимую библиотеку классов и использоваться повторно со всех поддерживаемых платформ .NET. (Это не так).

Из-за большого размера экрана приложение Windows 8 не имеет нескольких страниц — весь пользовательский интерфейс может поместиться на экране. Списки квартир связаны с  ListView контролем, и карта справа показывает их рядом.

Код, отвечающий за манипулирование моделью, не очень интересен, но фреймворк карт стоит отметить. Приложение Windows 8 использует элемент управления Bing Maps, для которого требуется ключ API, который вы получаете онлайн . На языке Bing Maps наложения называются кнопками, и вот как вы их размещаете на карте:

mapApartments.Children.Clear();
foreach (Apartment apartment in apartments)
{
  Pushpin pushpin = new Pushpin
  {
    Text = apartment.Bedrooms.ToString()
  };
  mapApartments.Children.Add(pushpin);
  Location location = new Location(
                    apartment.Latitude, apartment.Longitude);
  MapLayer.SetPosition(pushpin, location);
  pushpin.Tapped += (s, e) =>
  {
    mapApartments.SetView(location, 15);
  };
}

Серверные сценарии
На серверной стороне нам нужен серверный сценарий, чтобы обогатить наш список квартир географическими координатами. Пользователь предоставляет адрес, такой как «One Microsoft Way, Redmond WA», который мы должны преобразовать в пару широта-долгота. (Этот процесс называется геокодированием.)

Даже если каждая мобильная платформа поддерживает какой-либо сервис геокодирования (например, в Android это сервис геокодирования Google, предоставляемый через  Geocoder класс), было бы неплохо выполнить геокодирование на клиенте. Одна из причин заключается в том, что процесс геокодирования медленный и дорогой. Другая причина заключается в том, что на каждой платформе геокодирование одного и того же адреса может приводить к разным результатам, что крайне неинтуитивно. Вот почему эту работу лучше всего выгружать в бэкэнд службы.

В частности, вот соответствующая часть из скрипта вставки в  apartment таблице, который выполняет геокодирование с помощью бесплатного API геокодирования  Google с  использованием  request модуля Node.js,  предоставляемого Windows Azure Mobile Services:

function insert(item, user, request) {
  var reqModule = require('request');
  var base = 'http://maps.googleapis.com/maps/api/geocode/json?sensor=false'; 
  var what = escape(item.address);
  reqModule(base + '&address=' + what,
    function(error, response, body) {
      if (!error) {
        var geoResult = JSON.parse(body);
        var location = geoResult.results[0].geometry.location;
        item.latitude = location.lat;
        item.longitude = location.lng;
      }
      //Continue processing the request, omitted for brevity
    }
  );
}

Заключение На
этом мы завершаем наш вихревой тур по приложению Rent a Home, более конкретно его связанным с UI частям и используемым платформам карт. В следующей части мы рассмотрим, как аутентификация (с помощью Twitter) была интегрирована в приложение на всех четырех платформах, и как она затем использовалась для связи списков квартир с именем пользователя, который их добавил.