Вызов веб-API из приложения Windows Phone 8 (C#)
Роберт Макмюррей (Robert McMurray)
В этом руководстве вы узнаете, как создать полный комплексный сценарий, состоящий из веб-API ASP.NET приложения, которое предоставляет каталог книг для приложения Windows Phone 8.
Общие сведения
Службы RESTful, такие как веб-API ASP.NET упрощают создание приложений на основе HTTP для разработчиков, абстрагируя архитектуру для серверных и клиентских приложений. Вместо создания проприетарного протокола на основе сокетов для обмена данными разработчикам веб-API просто необходимо опубликовать необходимые методы HTTP для своего приложения (например, GET, POST, PUT, DELETE), а разработчикам клиентских приложений необходимо использовать только методы HTTP, необходимые для приложения.
В этом комплексном руководстве вы узнаете, как использовать веб-API для создания следующих проектов:
- В первой части этого руководства вы создадите приложение веб-API ASP.NET, которое поддерживает все операции создания, чтения, обновления и удаления (CRUD) для управления каталогом книг. Это приложение будет использовать пример XML-файла (books.xml) из MSDN.
- Во второй части этого руководства вы создадите интерактивное приложение Windows Phone 8, которое извлекает данные из приложения веб-API.
Предварительные требования
- Visual Studio 2013 с установленным пакетом SDK для Windows Phone 8
- Windows 8 или более поздней версии в 64-разрядной системе с установленным Hyper-V
- Список дополнительных требований см. в разделе Требования к системе на странице скачивания пакета SDK для Windows Phone 8.0.
Примечание
Если вы собираетесь проверить подключение между веб-API и проектами Windows Phone 8 в локальной системе, вам потребуется выполнить инструкции из статьи Подключение эмулятора Windows Phone 8 к приложениям веб-API на локальном компьютере, чтобы настроить среду тестирования.
Шаг 1. Создание проекта bookstore веб-API
Первым шагом этого комплексного руководства является создание проекта веб-API, который поддерживает все операции CRUD. Обратите внимание, что вы добавите проект приложения Windows Phone в это решение на шаге 2 этого руководства.
Откройте Visual Studio 2013.
Выберите Файл, Создать, а затем Проект.
Когда откроется диалоговое окно Новый проект , разверните узел Установленные, Затем Шаблоны, Visual C# и Интернет.
Щелкните изображение, чтобы развернуть Выделите ASP.NET веб-приложение, введите Имя проекта BookStore и нажмите кнопку ОК.
Когда откроется диалоговое окно Новый проект ASP.NET , выберите шаблон Веб-API и нажмите кнопку ОК.
Щелкните изображение, чтобы развернуть Когда откроется проект веб-API, удалите пример контроллера из проекта:
- Разверните папку Контроллеры в обозревателе решений.
- Щелкните правой кнопкой мыши файл ValuesController.cs и выберите команду Удалить.
- Нажмите кнопку ОК при появлении запроса на подтверждение удаления.
Добавление XML-файла данных в проект веб-API; Этот файл содержит содержимое каталога bookstore:
Щелкните правой кнопкой мыши папку App_Data в обозревателе решений, выберите команду Добавить, а затем — Новый элемент.
Когда откроется диалоговое окно Добавление нового элемента , выделите шаблон XML-файл .
Присвойте файлу имяBooks.xmlи нажмите кнопку Добавить.
После открытия файлаBooks.xml замените код в файле XML из примера файлаbooks.xml на сайте MSDN:
<?xml version="1.0" encoding="utf-8"?> <catalog> <book id="bk101"> <author>Gambardella, Matthew</author> <title>XML Developer's Guide</title> <genre>Computer</genre> <price>44.95</price> <publish_date>2000-10-01</publish_date> <description> An in-depth look at creating applications with XML. </description> </book> <book id="bk102"> <author>Ralls, Kim</author> <title>Midnight Rain</title> <genre>Fantasy</genre> <price>5.95</price> <publish_date>2000-12-16</publish_date> <description> A former architect battles corporate zombies, an evil sorceress, and her own childhood to become queen of the world. </description> </book> <book id="bk103"> <author>Corets, Eva</author> <title>Maeve Ascendant</title> <genre>Fantasy</genre> <price>5.95</price> <publish_date>2000-11-17</publish_date> <description> After the collapse of a nanotechnology society in England, the young survivors lay the foundation for a new society. </description> </book> <book id="bk104"> <author>Corets, Eva</author> <title>Oberon's Legacy</title> <genre>Fantasy</genre> <price>5.95</price> <publish_date>2001-03-10</publish_date> <description> In post-apocalypse England, the mysterious agent known only as Oberon helps to create a new life for the inhabitants of London. Sequel to Maeve Ascendant. </description> </book> <book id="bk105"> <author>Corets, Eva</author> <title>The Sundered Grail</title> <genre>Fantasy</genre> <price>5.95</price> <publish_date>2001-09-10</publish_date> <description> The two daughters of Maeve, half-sisters, battle one another for control of England. Sequel to Oberon's Legacy. </description> </book> <book id="bk106"> <author>Randall, Cynthia</author> <title>Lover Birds</title> <genre>Romance</genre> <price>4.95</price> <publish_date>2000-09-02</publish_date> <description> When Carla meets Paul at an ornithology conference, tempers fly as feathers get ruffled. </description> </book> <book id="bk107"> <author>Thurman, Paula</author> <title>Splish Splash</title> <genre>Romance</genre> <price>4.95</price> <publish_date>2000-11-02</publish_date> <description> A deep sea diver finds true love twenty thousand leagues beneath the sea. </description> </book> <book id="bk108"> <author>Knorr, Stefan</author> <title>Creepy Crawlies</title> <genre>Horror</genre> <price>4.95</price> <publish_date>2000-12-06</publish_date> <description> An anthology of horror stories about roaches, centipedes, scorpions and other insects. </description> </book> <book id="bk109"> <author>Kress, Peter</author> <title>Paradox Lost</title> <genre>Science Fiction</genre> <price>6.95</price> <publish_date>2000-11-02</publish_date> <description> After an inadvertant trip through a Heisenberg Uncertainty Device, James Salway discovers the problems of being quantum. </description> </book> <book id="bk110"> <author>O'Brien, Tim</author> <title>Microsoft .NET: The Programming Bible</title> <genre>Computer</genre> <price>36.95</price> <publish_date>2000-12-09</publish_date> <description> Microsoft's .NET initiative is explored in detail in this deep programmer's reference. </description> </book> <book id="bk111"> <author>O'Brien, Tim</author> <title>MSXML3: A Comprehensive Guide</title> <genre>Computer</genre> <price>36.95</price> <publish_date>2000-12-01</publish_date> <description> The Microsoft MSXML3 parser is covered in detail, with attention to XML DOM interfaces, XSLT processing, SAX and more. </description> </book> <book id="bk112"> <author>Galos, Mike</author> <title>Visual Studio 7: A Comprehensive Guide</title> <genre>Computer</genre> <price>49.95</price> <publish_date>2001-04-16</publish_date> <description> Microsoft Visual Studio 7 is explored in depth, looking at how Visual Basic, Visual C++, C#, and ASP+ are integrated into a comprehensive development environment. </description> </book> </catalog>
Сохраните и закройте XML-файл.
Добавьте модель bookstore в проект веб-API; Эта модель содержит логику создания, чтения, обновления и удаления (CRUD) для приложения bookstore:
Щелкните правой кнопкой мыши папку Models в обозревателе решений, выберите команду Добавить, а затем — Класс.
Когда откроется диалоговое окно Добавление нового элемента , присвойте файлу класса имя BookDetails.cs и нажмите кнопку Добавить.
При открытии файла BookDetails.cs замените код в файле следующим кодом:
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Xml; using System.Xml.Linq; using System.Xml.XPath; using System.Web; namespace BookStore.Models { /// <summary> /// Define a class that will hold the detailed information for a book. /// </summary> public class BookDetails { [Required] public String Id { get; set; } [Required] public String Title { get; set; } public String Author { get; set; } public String Genre { get; set; } public Decimal Price { get; set; } public DateTime PublishDate { get; set; } public String Description { get; set; } } /// <summary> /// Define an interface which contains the methods for the book repository. /// </summary> public interface IBookRepository { BookDetails CreateBook(BookDetails book); IEnumerable<BookDetails> ReadAllBooks(); BookDetails ReadBook(String id); BookDetails UpdateBook(String id, BookDetails book); Boolean DeleteBook(String id); } /// <summary> /// Define a class based on the book repository interface which contains the method implementations. /// </summary> public class BookRepository : IBookRepository { private string xmlFilename = null; private XDocument xmlDocument = null; /// <summary> /// Define the class constructor. /// </summary> public BookRepository() { try { // Determine the path to the books.xml file. xmlFilename = HttpContext.Current.Server.MapPath("~/app_data/books.xml"); // Load the contents of the books.xml file into an XDocument object. xmlDocument = XDocument.Load(xmlFilename); } catch (Exception ex) { // Rethrow the exception. throw ex; } } /// <summary> /// Method to add a new book to the catalog. /// Defines the implementation of the POST method. /// </summary> public BookDetails CreateBook(BookDetails book) { try { // Retrieve the book with the highest ID from the catalog. var highestBook = ( from bookNode in xmlDocument.Elements("catalog").Elements("book") orderby bookNode.Attribute("id").Value descending select bookNode).Take(1); // Extract the ID from the book data. string highestId = highestBook.Attributes("id").First().Value; // Create an ID for the new book. string newId = "bk" + (Convert.ToInt32(highestId.Substring(2)) + 1).ToString(); // Verify that this book ID does not currently exist. if (this.ReadBook(newId) == null) { // Retrieve the parent element for the book catalog. XElement bookCatalogRoot = xmlDocument.Elements("catalog").Single(); // Create a new book element. XElement newBook = new XElement("book", new XAttribute("id", newId)); // Create elements for each of the book's data items. XElement[] bookInfo = FormatBookData(book); // Add the element to the book element. newBook.ReplaceNodes(bookInfo); // Append the new book to the XML document. bookCatalogRoot.Add(newBook); // Save the XML document. xmlDocument.Save(xmlFilename); // Return an object for the newly-added book. return this.ReadBook(newId); } } catch (Exception ex) { // Rethrow the exception. throw ex; } // Return null to signify failure. return null; } /// <summary> /// Method to retrieve all of the books in the catalog. /// Defines the implementation of the non-specific GET method. /// </summary> public IEnumerable<BookDetails> ReadAllBooks() { try { // Return a list that contains the catalog of book ids/titles. return ( // Query the catalog of books. from book in xmlDocument.Elements("catalog").Elements("book") // Sort the catalog based on book IDs. orderby book.Attribute("id").Value ascending // Create a new instance of the detailed book information class. select new BookDetails { // Populate the class with data from each of the book's elements. Id = book.Attribute("id").Value, Author = book.Element("author").Value, Title = book.Element("title").Value, Genre = book.Element("genre").Value, Price = Convert.ToDecimal(book.Element("price").Value), PublishDate = Convert.ToDateTime(book.Element("publish_date").Value), Description = book.Element("description").Value }).ToList(); } catch (Exception ex) { // Rethrow the exception. throw ex; } } /// <summary> /// Method to retrieve a specific book from the catalog. /// Defines the implementation of the ID-specific GET method. /// </summary> public BookDetails ReadBook(String id) { try { // Retrieve a specific book from the catalog. return ( // Query the catalog of books. from book in xmlDocument.Elements("catalog").Elements("book") // Specify the specific book ID to query. where book.Attribute("id").Value.Equals(id) // Create a new instance of the detailed book information class. select new BookDetails { // Populate the class with data from each of the book's elements. Id = book.Attribute("id").Value, Author = book.Element("author").Value, Title = book.Element("title").Value, Genre = book.Element("genre").Value, Price = Convert.ToDecimal(book.Element("price").Value), PublishDate = Convert.ToDateTime(book.Element("publish_date").Value), Description = book.Element("description").Value }).Single(); } catch { // Return null to signify failure. return null; } } /// <summary> /// Populates a book BookDetails class with the data for a book. /// </summary> private XElement[] FormatBookData(BookDetails book) { XElement[] bookInfo = { new XElement("author", book.Author), new XElement("title", book.Title), new XElement("genre", book.Genre), new XElement("price", book.Price.ToString()), new XElement("publish_date", book.PublishDate.ToString()), new XElement("description", book.Description) }; return bookInfo; } /// <summary> /// Method to update an existing book in the catalog. /// Defines the implementation of the PUT method. /// </summary> public BookDetails UpdateBook(String id, BookDetails book) { try { // Retrieve a specific book from the catalog. XElement updateBook = xmlDocument.XPathSelectElement(String.Format("catalog/book[@id='{0}']", id)); // Verify that the book exists. if (updateBook != null) { // Create elements for each of the book's data items. XElement[] bookInfo = FormatBookData(book); // Add the element to the book element. updateBook.ReplaceNodes(bookInfo); // Save the XML document. xmlDocument.Save(xmlFilename); // Return an object for the updated book. return this.ReadBook(id); } } catch (Exception ex) { // Rethrow the exception. throw ex; } // Return null to signify failure. return null; } /// <summary> /// Method to remove an existing book from the catalog. /// Defines the implementation of the DELETE method. /// </summary> public Boolean DeleteBook(String id) { try { if (this.ReadBook(id) != null) { // Remove the specific child node from the catalog. xmlDocument .Elements("catalog") .Elements("book") .Where(x => x.Attribute("id").Value.Equals(id)) .Remove(); // Save the XML document. xmlDocument.Save(xmlFilename); // Return a success status. return true; } else { // Return a failure status. return false; } } catch (Exception ex) { // Rethrow the exception. throw ex; } } } }
Сохраните и закройте файл BookDetails.cs .
Добавьте контроллер bookstore в проект веб-API:
Щелкните правой кнопкой мыши папку Контроллеры в обозревателе решений, выберите команду Добавить, а затем контроллер.
Когда откроется диалоговое окно Добавление шаблона , выберите Контроллер Веб-API 2 — пустой и нажмите кнопку Добавить.
Когда откроется диалоговое окно Добавление контроллера , присвойте контроллеру имя BooksController и нажмите кнопку Добавить.
При открытии файла BooksController.cs замените код в файле следующим кодом:
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; using BookStore.Models; namespace BookStore.Controllers { public class BooksController : ApiController { private BookRepository repository = null; // Define the class constructor. public BooksController() { this.repository = new BookRepository(); } /// <summary> /// Method to retrieve all of the books in the catalog. /// Example: GET api/books /// </summary> [HttpGet] public HttpResponseMessage Get() { IEnumerable<BookDetails> books = this.repository.ReadAllBooks(); if (books != null) { return Request.CreateResponse<IEnumerable<BookDetails>>(HttpStatusCode.OK, books); } else { return Request.CreateResponse(HttpStatusCode.NotFound); } } /// <summary> /// Method to retrieve a specific book from the catalog. /// Example: GET api/books/5 /// </summary> [HttpGet] public HttpResponseMessage Get(String id) { BookDetails book = this.repository.ReadBook(id); if (book != null) { return Request.CreateResponse<BookDetails>(HttpStatusCode.OK, book); } else { return Request.CreateResponse(HttpStatusCode.NotFound); } } /// <summary> /// Method to add a new book to the catalog. /// Example: POST api/books /// </summary> [HttpPost] public HttpResponseMessage Post(BookDetails book) { if ((this.ModelState.IsValid) && (book != null)) { BookDetails newBook = this.repository.CreateBook(book); if (newBook != null) { var httpResponse = Request.CreateResponse<BookDetails>(HttpStatusCode.Created, newBook); string uri = Url.Link("DefaultApi", new { id = newBook.Id }); httpResponse.Headers.Location = new Uri(uri); return httpResponse; } } return Request.CreateResponse(HttpStatusCode.BadRequest); } /// <summary> /// Method to update an existing book in the catalog. /// Example: PUT api/books/5 /// </summary> [HttpPut] public HttpResponseMessage Put(String id, BookDetails book) { if ((this.ModelState.IsValid) && (book != null) && (book.Id.Equals(id))) { BookDetails modifiedBook = this.repository.UpdateBook(id, book); if (modifiedBook != null) { return Request.CreateResponse<BookDetails>(HttpStatusCode.OK, modifiedBook); } else { return Request.CreateResponse(HttpStatusCode.NotFound); } } return Request.CreateResponse(HttpStatusCode.BadRequest); } /// <summary> /// Method to remove an existing book from the catalog. /// Example: DELETE api/books/5 /// </summary> [HttpDelete] public HttpResponseMessage Delete(String id) { BookDetails book = this.repository.ReadBook(id); if (book != null) { if (this.repository.DeleteBook(id)) { return Request.CreateResponse(HttpStatusCode.OK); } } else { return Request.CreateResponse(HttpStatusCode.NotFound); } return Request.CreateResponse(HttpStatusCode.BadRequest); } } }
Сохраните и закройте файл BooksController.cs .
Создайте приложение веб-API для проверка ошибок.
Шаг 2. Добавление проекта каталога bookstore Windows Phone 8
Следующим шагом этого комплексного сценария является создание приложения каталога для Windows Phone 8. Это приложение будет использовать шаблон приложения Windows Phone databound для пользовательского интерфейса по умолчанию, а также приложение веб-API, созданное на шаге 1 этого руководства, в качестве источника данных.
Щелкните правой кнопкой мыши решение BookStore в обозревателе решений, а затем выберите Добавить, а затем — Создать проект.
Когда откроется диалоговое окно Новый проект, разверните узел Установленные, затем Visual C#, а затем Windows Phone.
Выделите Windows Phone Databound App, введите Имя BookCatalog и нажмите кнопку ОК.
Добавьте пакет NuGet Json.NET в проект BookCatalog :
- Щелкните правой кнопкой мыши ссылки для проекта BookCatalog в обозревателе решений и выберите пункт Управление пакетами NuGet.
- Когда откроется диалоговое окно Управление пакетами NuGet , разверните раздел Интернет и выделите nuget.org.
- Введите Json.NET в поле поиска и щелкните значок поиска.
- Выделите Json.NET в результатах поиска и нажмите кнопку Установить.
- После завершения установки нажмите кнопку Закрыть.
Добавьте модель BookDetails в проект BookCatalog ; содержит универсальную модель класса bookstore:
Щелкните правой кнопкой мыши проект BookCatalog в обозревателе решений, выберите добавить, а затем — Создать папку.
Присвойте новой папке имя Models.
Щелкните правой кнопкой мыши папку Models в обозревателе решений, выберите команду Добавить, а затем — Класс.
Когда откроется диалоговое окно Добавление нового элемента , присвойте файлу класса имя BookDetails.cs и нажмите кнопку Добавить.
При открытии файла BookDetails.cs замените код в файле следующим кодом:
using System; using System.Text; namespace BookCatalog.Models { /// <summary> /// Define a class that will hold the detailed information for a book. /// </summary> public class BookDetails { public String Id { get; set; } public String Title { get; set; } public String Author { get; set; } public String Genre { get; set; } public Decimal Price { get; set; } public DateTime PublishDate { get; set; } public String Description { get; set; } } }
Сохраните и закройте файл BookDetails.cs .
Обновите класс MainViewModel.cs , чтобы включить функции для взаимодействия с приложением веб-API BookStore:
Разверните папку ViewModels в обозревателе решений и дважды щелкните файл MainViewModel.cs .
При открытии файла MainViewModel.cs замените код в файле следующим кодом: Обратите внимание, что необходимо обновить значение
apiUrl
константы, указав фактический URL-адрес веб-API:using System; using System.Collections.ObjectModel; using System.ComponentModel; using System.Net; using System.Net.NetworkInformation; using BookCatalog.Resources; using System.Collections.Generic; using Newtonsoft.Json; using BookCatalog.Models; namespace BookCatalog.ViewModels { public class MainViewModel : INotifyPropertyChanged { const string apiUrl = @"http://www.contoso.com/api/Books"; public MainViewModel() { this.Items = new ObservableCollection<ItemViewModel>(); } /// <summary> /// A collection for ItemViewModel objects. /// </summary> public ObservableCollection<ItemViewModel> Items { get; private set; } public bool IsDataLoaded { get; private set; } /// <summary> /// Creates and adds a few ItemViewModel objects into the Items collection. /// </summary> public void LoadData() { if (this.IsDataLoaded == false) { this.Items.Clear(); this.Items.Add(new ItemViewModel() { ID = "0", LineOne = "Please Wait...", LineTwo = "Please wait while the catalog is downloaded from the server.", LineThree = null }); WebClient webClient = new WebClient(); webClient.Headers["Accept"] = "application/json"; webClient.DownloadStringCompleted += new DownloadStringCompletedEventHandler(webClient_DownloadCatalogCompleted); webClient.DownloadStringAsync(new Uri(apiUrl)); } } private void webClient_DownloadCatalogCompleted(object sender, DownloadStringCompletedEventArgs e) { try { this.Items.Clear(); if (e.Result != null) { var books = JsonConvert.DeserializeObject<BookDetails[]>(e.Result); int id = 0; foreach (BookDetails book in books) { this.Items.Add(new ItemViewModel() { ID = (id++).ToString(), LineOne = book.Title, LineTwo = book.Author, LineThree = book.Description.Replace("\n", " ") }); } this.IsDataLoaded = true; } } catch (Exception ex) { this.Items.Add(new ItemViewModel() { ID = "0", LineOne = "An Error Occurred", LineTwo = String.Format("The following exception occured: {0}", ex.Message), LineThree = String.Format("Additional inner exception information: {0}", ex.InnerException.Message) }); } } public event PropertyChangedEventHandler PropertyChanged; private void NotifyPropertyChanged(String propertyName) { PropertyChangedEventHandler handler = PropertyChanged; if (null != handler) { handler(this, new PropertyChangedEventArgs(propertyName)); } } } }
Сохраните и закройте файл MainViewModel.cs .
Обновите файл MainPage.xaml , чтобы настроить имя приложения:
Дважды щелкните файл MainPage.xaml в обозревателе решений.
Открыв файл MainPage.xaml , найдите следующие строки кода:
<StackPanel Grid.Row="0" Margin="12,17,0,28"> <TextBlock Text="MY APPLICATION" Style="{StaticResource PhoneTextNormalStyle}"/> <TextBlock Text="page name" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/> </StackPanel>
Замените эти строки следующим кодом:
<StackPanel Grid.Row="0" Margin="12,17,0,28"> <TextBlock Text="Book Store" Style="{StaticResource PhoneTextTitle1Style}"/> <TextBlock Text="Current Catalog" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle2Style}"/> </StackPanel>
Сохраните и закройте файл MainPage.xaml .
Обновите файл DetailsPage.xaml , чтобы настроить отображаемые элементы:
Дважды щелкните файл DetailsPage.xaml в обозревателе решений.
Открыв файл DetailsPage.xaml , найдите следующие строки кода:
<StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28"> <TextBlock Text="MY APPLICATION" Style="{StaticResource PhoneTextNormalStyle}"/> <TextBlock Text="{Binding LineOne}" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/> </StackPanel>
Замените эти строки следующим кодом:
<StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28"> <TextBlock Text="Book Store" Style="{StaticResource PhoneTextTitle1Style}"/> <TextBlock Text="{Binding LineOne}" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle2Style}"/> </StackPanel>
Сохраните и закройте файл DetailsPage.xaml .
Создайте приложение Windows Phone для проверка ошибок.
Шаг 3. Тестирование комплексного решения
Как упоминалось в разделе Предварительные требования этого руководства, при тестировании подключения между веб-API и проектами Windows Phone 8 в локальной системе необходимо выполнить инструкции из статьи Подключение эмулятора Windows Phone 8 к приложениям веб-API на локальном компьютере, чтобы настроить среду тестирования.
После настройки среды тестирования необходимо задать Windows Phone приложение в качестве запускаемого проекта. Для этого выделите приложение BookCatalog в обозревателе решений и нажмите кнопку Задать в качестве запускаемого проекта:
Щелкните изображение, чтобы развернуть |
При нажатии клавиши F5 Visual Studio запустит эмулятор Windows Phone, который отобразит сообщение "Подождите", пока данные приложения будут получены из веб-API:
Щелкните изображение, чтобы развернуть |
Если все прошло успешно, отобразится каталог:
Щелкните изображение, чтобы развернуть |
Если коснуться названия книги, приложение отобразит описание книги:
Щелкните изображение, чтобы развернуть |
Если приложению не удается связаться с веб-API, отобразится сообщение об ошибке:
Щелкните изображение, чтобы развернуть |
Если нажать на сообщение об ошибке, отобразятся все дополнительные сведения об ошибке:
Щелкните изображение, чтобы развернуть |