从 Windows Phone 8 应用程序调用 Web API (C#)
本教程介绍如何创建包含 ASP.NET Web API应用程序的完整端到端方案,该应用程序为 Windows Phone 8 应用程序提供书籍目录。
概述
RESTful 服务(如 ASP.NET Web API)通过抽象化服务器端和客户端应用程序的体系结构来简化开发人员基于 HTTP 的应用程序的创建。 Web API 开发人员只需为其 (应用程序发布必需的 HTTP 方法(例如 GET、POST、PUT、DELETE) ),而客户端应用程序开发人员只需使用其应用程序所需的 HTTP 方法,而不是创建基于套接字的专有协议。
本端到端教程介绍如何使用 Web API 创建以下项目:
- 在本教程的第一部分中,你将创建一个 ASP.NET Web API应用程序,该应用程序支持用于管理书籍目录的所有创建、读取、更新和删除 (CRUD) 操作。 此应用程序将使用来自 MSDN 的示例 XML 文件 (books.xml) 。
- 在本教程的第二部分中,你将创建一个交互式 Windows Phone 8 应用程序,用于从 Web API 应用程序检索数据。
先决条件
- 安装了 Windows Phone 8 SDK 的Visual Studio 2013
- 安装了 Hyper-V 的 64 位系统上的 Windows 8 或更高版本
- 有关其他要求的列表,请参阅 Windows Phone SDK 8.0 下载页上的“系统要求”部分。
注意
如果要测试本地系统上的 Web API 与 Windows Phone 8 个项目之间的连接,则需要按照将 Windows Phone 8 模拟器连接到本地计算机上的 Web API 应用程序一文中的说明设置测试环境。
步骤 1:创建 Web API Bookstore 项目
本端到端教程的第一步是创建支持所有 CRUD 操作的 Web API 项目;请注意,将在本教程的步骤 2 中将 Windows Phone 应用程序项目添加到此解决方案。
打开Visual Studio 2013。
依次单击“ 文件”、“ 新建”、“ 项目”。
显示“新建项目”对话框时,依次展开“已安装”、“模板”、“Visual C#”和“Web”。
单击图像展开 突出显示 “ASP.NET Web 应用程序”,输入 BookStore 为项目名称,然后单击“ 确定”。
显示“ 新建 ASP.NET 项目 ”对话框时,选择 Web API 模板,然后单击“ 确定”。
单击图像展开 当 Web API 项目打开时,从项目中删除示例控制器:
- 在解决方案资源管理器中展开 Controllers 文件夹。
- 右键单击 ValuesController.cs 文件,然后单击“ 删除”。
- 出现提示时,单击“ 确定 ”以确认删除。
将 XML 数据文件添加到 Web API 项目;此文件包含书店目录的内容:
在解决方案资源管理器中右键单击 App_Data 文件夹,单击“ 添加”,然后单击“ 新建项”。
显示“ 添加新项 ”对话框时,突出显示 XML 文件 模板。
将文件 命名为Books.xml,然后单击“ 添加”。
打开 Books.xml 文件时,将 文件中的代码替换为 MSDN 上示例 books.xml 文件中的 XML:
<?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 模型添加到 Web API 项目;此模型包含 bookstore 应用程序的创建、读取、更新和删除 (CRUD) 逻辑:
右键单击解决方案资源管理器中的 “模型 ”文件夹,单击“ 添加”,然后单击“ 类”。
显示“ 添加新项 ”对话框时,将类文件命名为 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 控制器添加到 Web API 项目:
在解决方案资源管理器中右键单击 Controllers 文件夹,单击“ 添加”,然后单击“ 控制器”。
显示“ 添加基架 ”对话框时,突出显示 “Web 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 文件。
生成 Web API 应用程序以检查错误。
步骤 2:添加Windows Phone 8 Bookstore 目录项目
此端到端方案的下一步是为 Windows Phone 8 创建目录应用程序。 此应用程序将使用默认用户界面的 Windows Phone Databound 应用模板,并将在本教程的步骤 1 中创建的 Web API 应用程序用作数据源。
在解决方案资源管理器的 中右键单击 BookStore 解决方案,单击“ 添加”,然后单击“ 新建项目”。
显示“新建项目”对话框时,依次展开“已安装”、“Visual C#”,然后Windows Phone。
突出显示Windows Phone Databound App,输入 BookCatalog 为名称,然后单击“确定”。
将 Json.NET NuGet 包添加到 BookCatalog 项目:
- 在解决方案资源管理器中右键单击 BookCatalog 项目的引用,然后单击“管理 NuGet 包”。
- 显示“ 管理 NuGet 包 ”对话框时,展开“ 联机 ”部分,并突出显示 nuget.org。
- 在搜索字段中输入 Json.NET ,然后单击搜索图标。
- 在搜索结果中突出显示 Json.NET ,然后单击“ 安装”。
- 安装完成后,单击“ 关闭”。
将 BookDetails 模型添加到 BookCatalog 项目;这包含 bookstore 类的泛型模型:
在解决方案资源管理器中右键单击 BookCatalog 项目,单击“ 添加”,然后单击“ 新建文件夹”。
将新文件夹命名为 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 类,以包含与 BookStore Web API 应用程序通信的功能:
在解决方案资源管理器中展开 ViewModels 文件夹,然后双击 MainViewModel.cs 文件。
打开 MainViewModel.cs 文件时,将 文件中的代码替换为以下内容:请注意,需要使用 Web API 的实际 URL 更新 常量的值
apiUrl
: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:测试端到端解决方案
如本教程的先决条件部分所述,在测试 Web API 与本地系统上Windows Phone 8 个项目之间的连接时,需要按照将 Windows Phone 8 仿真器连接到本地计算机上的 Web API 应用程序一文中的说明设置测试环境。
配置测试环境后,需要将Windows Phone应用程序设置为启动项目。 为此,请在解决方案资源管理器中突出显示 BookCatalog 应用程序,然后单击“ 设为启动项目”:
![]() |
---|
单击图像展开 |
按 F5 时,Visual Studio 将启动Windows Phone模拟器,从 Web API 检索应用程序数据时,将显示“请稍候”消息:
![]() |
---|
单击图像展开 |
如果一切成功,应会看到显示目录:
![]() |
---|
单击图像展开 |
如果点击任何书名,应用程序将显示书籍说明:
![]() |
---|
单击图像展开 |
如果应用程序无法与 Web API 通信,将显示一条错误消息:
![]() |
---|
单击图像展开 |
如果点击错误消息,将显示有关错误的任何其他详细信息:
![]() |
---|
单击图像展开 |