教程:创建 OneNote 应用
适用于:OneDrive 上的消费者笔记本 | Office 365 上的企业级笔记本
本教程将向你介绍如何创建一个使用 OneNote API 获取和创建 OneNote 内容的简单应用。 我们将创建的应用使用 OneNote REST API 进行两次调用:
获取 10 个最近修改的分区的名称和 ID。
GET ../notes/sections?select=name,id&top=10&orderby=lastModifiedTime%20desc
在特定的分区创建一个页面。
POST ../notes/sections/{id}/pages
选择平台:
备注
本教程旨在演示如何访问 OneNote API,但它不包含生产就绪代码。 在创建应用时,请仔细阅读代码以了解潜在的安全性、验证和其他代码质量问题。
为 iOS 创建 OneNote 应用
该应用使用 适用于 iOS 的 OneDrive SDK 处理身份验证和网络调用。
先决条件
要执行本教程的步骤,你需要以下各项:
- Xcode 7(来自 Apple)。
- CocoaPods 依赖关系管理器。 如果你不熟悉 CocoaPods 的使用,请参阅 CocoaPods指南。
- 在 Azure 管理门户上注册本机客户端应用,或在 Microsoft 帐户开发人员中心上注册移动客户端应用。 (了解如何注册应用。)
为 iOS 创建 OneNote 应用:
教程结尾提供了关键示例文件的完整代码示例。
在 Xcode 中创建项目
在 Xcode 中,为 iOS 创建一个名为 OneNote-iOS-App 的单视图应用。 选择“Objective C”,并选择 iPhone 设备。
项目创建完成后,关闭 Xcode。 创建 Podfile 后,你将打开工作区。
添加 OneDrive SDK 依赖项
本教程中的应用为 Microsoft 帐户(以前称为 Live Connect)和 Azure Active Directory 身份验证均使用 OneDrive SDK。 Microsoft 帐户身份验证用于访问 OneDrive 上的消费者笔记本。 Azure AD 身份验证用于访问 Office 365 上的企业级笔记本。
在终端中运行这些命令来创建 Podfile,并在 Xcode 中打开文件。
cd {path to the OneNote-iOS-App project directory} touch podfile open -a xcode podfile
将 OneDrive SDK 依赖项添加到 Podfile,然后保存文件。
pod 'OneDriveSDK'
在终端中运行这些命令来安装依赖项,并在 Xcode 中打开项目工作区。 安装完成时,你应会收到确认。
pod install open onenote-ios-app.xcworkspace/
适用于 iOS 9.0 的 Xcode 7 应用
如果你使用 Xcode 7 以 iOS 9 为目标,则需要启用 PFS 异常。 请参阅适用于 iOS 的 OneDrive SDK 自述文件 中的 ** iOS 9 应用传输安全性**部分了解相关说明。
构建 UI
添加一个 选取器,显示用户最近修改的 10 个分区,以及一个在用于所选分区中创建 OneNote 页面的按钮。
在 Xcode 中,打开 Main.storyboard 并将大小类控件(画布下方)更改为 wCompact/hAny。
将选取器视图和按钮从对象库拖到画布上。 为按钮文本使用 “创建页面”。
为选取器视图创建连接:
a. 按住 Control 键将选取器视图拖到画布上方的“视图控制器”图标。 选择 “数据源” 出口。
b. 为 “委托” 出口重复这些步骤。
c. 选择 “查看”>“助理编辑器”>“显示助理编辑器”,并在第二个窗口中打开 ViewController.h。
d. 按住 Control 键并将画布中的选取器视图拖到 @interface 代码块。 插入一个名为sectionPicker 的出口连接。为按钮创建连接:
a. 按住 Control 键并将画布中的按钮拖到 @interface 代码块。 插入一个名为 createPageButton 的出口连接。
b. 在辅助编辑器中打开 ViewController.m。
c. 按住 Control 键并将画布中的按钮拖到 @implementation 代码块。 为 Touch Up Inside 事件插入一个名为 createPage 的 “操作” 连接。声明 UIPickerViewDelegate 和 UIPickerViewDataSource 协议。
ViewController.h 应如下所示:
import <UIKit/UIKit.h> @interface ViewController : UIViewController<UIPickerViewDelegate, UIPickerViewDataSource> @property (weak, nonatomic) IBOutlet UIPickerView *sectionPicker; @property (weak, nonatomic) IBOutlet UIButton *createPageButton; @end
在 ViewController.m 中,为选取器视图添加下列委托方法。
#pragma mark - Delegate Methods -(NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView { return 1; } -(NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component { return sectionNamesForPicker.count; } -(NSString *)pickerView:(UIPickerView*)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component { return [sectionNamesForPicker objectAtIndex:row]; }
不要担心 sectionNamesForPicker 的错误。 我们稍后将添加该变量。
在 viewDidLoad 方法中,在第
[super viewDidLoad]
行后添加用于连接选取器的代码。self.sectionPicker.delegate = self; self.sectionPicker.dataSource = self;
添加身份验证支持
OneDrive SDK 为你处理身份验证和授权。 你只需要为你的应用程序提供标识符,然后使用 ODClient。 SDK 将在用户首次运行应用时调用登录 UI,然后存储帐户信息。 (详细了解在 SDK 中进行身份验证。)
在 AppDelegate.m 中,导入 OneDrive SDK。
#import <OneDriveSDK/OneDriveSDK.h>
将 didFinishLaunchingWithOptions 方法替换为以下代码。
然后,将占位符属性值替换为注册应用的信息。 如果只使用一个应用进行测试,你可以注释掉不使用的属性。
(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Set the client ID and permission scopes of your app registered on the Microsoft account Developer Center. static NSString *const msaClientId = @"000000001A123456"; static NSString *const msaScopesString = @"wl.signin,wl.offline_access,office.onenote_update"; // Set the client ID and redirect URI of your app registered on the Azure Management Portal. static NSString *const aadClientId = @"0b18d05c-386d-4133-b481-az1234567890"; static NSString *const aadRedirectUri = @"https://localhost/"; // Set properties on the ODClient. NSArray *const msaScopes = [msaScopesString componentsSeparatedByString:@","]; [ODClient setMicrosoftAccountAppId:msaClientId scopes:msaScopes]; [ODClient setActiveDirectoryAppId:aadClientId capability:@"Notes" redirectURL:aadRedirectUri]; return YES; }
备注
此应用允许你一次登录一个帐户(Microsof t帐户或者工作或学校帐户)。 要了解如何同时支持两种帐户类型并存储多个帐户,请参阅 CloudRoll 示例。
在 ViewController.h 中,导入 OneDrive SDK 并为 ODClient 对象声明一个属性。 所有对 SDK 的调用都是通过 ODClient 对象完成的。
a. 添加导入语句:
#import <OneDriveSDK/OneDriveSDK.h>
b. 将 client 属性添加到 @interface 代码块。
@property (strong, nonatomic) ODClient *client;
在 ViewController.m 中,将以下代码添加到 viewDidLoad 方法来获得经过身份验证的ODClient。
SDK 将在用户首次运行应用时调用登录 UI,然后存储帐户信息。
[ODClient clientWithCompletion:^(ODClient *odClient, NSError *error) { if (!error){ self.client = odClient; [self getSections]; } else { NSLog(@"Error with auth: %@", [error localizedDescription]); } }];
我们将在下一部分中添加 getSections 方法。
在 ViewController.m 中,将 sendRequest 方法添加到 @implementation 代码块。
此方法将所需的 Authorization 标头添加到 GET sections 和 POST pages 请求,并创建数据传输任务。
// Send the request. - (void)sendRequest:(NSMutableURLRequest *)request { // Add the required Authorization header with access token. [self.client.authProvider appendAuthHeaders:request completion:^(NSMutableURLRequest *requests, NSError *error) { // This app also uses the OneDrive SDK to send HTTP requests. [[self.client.httpProvider dataTaskWithRequest:(request) completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { [self handleResponse:data response:response error:error]; }] resume]; }]; }
现在你已准备好调用 OneNote 服务。
调用 OneNote API
当应用加载时,它会获取 10 个最近修改的分区的名称和 ID,并使用分区名称填充选取器。 将在选定分区中创建新页面。
在 ViewController.h 中,添加存储响应的属性。
// Variables to store the response data. @property (strong, nonatomic) NSHTTPURLResponse *returnResponse; @property (strong, nonatomic) NSMutableData *returnData;
将以下步骤中的所有代码添加到 ViewController.m 中的 @implementation 代码块。 不要担心在创建应用时看到的错误。 代码完成后,错误将会消失。
在 ViewController.m 中,添加 OneNote 服务根 URL 变量、分区名称和 ID 字典以及用来填充选择器的分区名称数组。
static NSString const *serviceRootUrl = @"https://www.onenote.com/api/v1.0/me/notes/"; NSMutableDictionary *sectionNamesAndIds; NSArray *sectionNamesForPicker;
添加 getSections 方法来构建 GET sections 请求。
// Build the "GET sections" request. - (void)getSections { // Construct the request URI and the request. NSString *sectionsEndpoint = [serviceRootUrl stringByAppendingString:@"sections?select=name,id&top=10&orderby=lastModifiedTime%20desc"]; NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[[NSURL alloc] initWithString:sectionsEndpoint]]; request.HTTPMethod = @"GET"; if (self.client) { // Send the HTTP request. [self sendRequest:request]; } _createPageButton.enabled = false; }
添加 handleResponse 方法来处理来自 GET sections 和 POST pages 请求的响应。
// Handle the response. - (void)handleResponse:(NSData *)data response:(NSURLResponse *)response error:(NSError *) error { // Log the response. NSLog(@"Response %@ with error %@.\n", response, error); NSString *stringData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; NSLog(@"Body: %@.\n", stringData); // Store the response. self.returnData = [[NSMutableData alloc] init]; NSMutableData *convertedData = [data mutableCopy]; [self.returnData appendData:convertedData]; self.returnResponse = (NSHTTPURLResponse *)response; NSInteger status = [self.returnResponse statusCode]; // Check for "GET sections" success. if (status == 200) { NSLog(@"Sections retrieved!\n"); // Get the section data and populate the picker. [self getSectionNamesAndIds]; } // Check for "POST pages" success. else if (status == 201) { NSLog(@"Page created!\n"); // Get the page object and parse out some properties. NSDictionary *pageProperties = [self convertData]; NSString *selfLink = [pageProperties objectForKey:@"self"]; NSDictionary *links = [pageProperties objectForKey:@"links"]; NSString *clientUrl = [[links objectForKey:@"oneNoteClientUrl"] objectForKey:@"href"]; NSString *webUrl = [[links objectForKey:@"oneNoteWebUrl"] objectForKey:@"href"]; NSLog(@"Link to new page endpoint: %@\n", selfLink); NSLog(@"Link open page in the installed client: %@\n", clientUrl); NSLog(@"Link to open page in OneNote Online: %@\n", webUrl); } else { NSLog(@"Status code: %ld. Check the logged response for more information.", (long)status); } }
添加 convertData 方法将响应数据转换为JSON。
// Get the OneNote entity data from the response. - (NSDictionary *)convertData { // Convert the message body to JSON. NSError *parseError; NSDictionary *data = [NSJSONSerialization JSONObjectWithData:self.returnData options:kNilOptions error:&parseError]; if (!parseError) { return data; } else { NSLog(@"Error parsing response: %@", [parseError localizedDescription]); return nil; } }
添加 getSectionNamesAndIds 方法来存储分区名称和 ID 并填充选择器。
// Store the section names and IDs, and populate the section picker. - (void)getSectionNamesAndIds { // Get the "value" array that contains the returned sections. NSDictionary *results = [self convertData]; // Add the name-id pairs to sectionNamesAndIds, which is used to map section names to IDs. if ([results objectForKey:@"value"] != nil) { NSDictionary *sections =[results objectForKey:@"value"]; sectionNamesAndIds = [[NSMutableDictionary alloc] init]; for (NSMutableDictionary *dict in sections) { NSString *sectionName = [dict objectForKey:@"name"]; NSString *sectionId = [dict objectForKey:@"id"]; sectionNamesAndIds[sectionName] = sectionId; } } // Populate the picker with the section names. sectionNamesForPicker = [sectionNamesAndIds allKeys]; dispatch_async(dispatch_get_main_queue(), ^{[_sectionPicker reloadComponent:0];}); _createPageButton.enabled = true; }
编辑在添加按钮操作时创建的 createPage 方法。 此代码将创建一个简单的 HTML 页面。
// Create a simple page. - (IBAction)createPage:(id)sender { // Get the ID of the section that's selected in the picker. NSInteger row = [self.sectionPicker selectedRowInComponent:0]; NSString *selectedSectionName = sectionNamesForPicker[row]; NSString *selectedSectionId = sectionNamesAndIds[selectedSectionName]; // Construct the request URI and the request. NSString *pagesEndpoint = [NSString stringWithFormat:@"sections/%@/pages", selectedSectionId]; NSString *fullEndpoint = [serviceRootUrl stringByAppendingString:pagesEndpoint]; NSString *date = [self formatDate]; NSString *simpleHtml = [NSString stringWithFormat:@"<html>" "<head>" "<title>A page created from simple HTML from iOS</title>" "<meta name=\"created\" content=\"%@\" />" "</head>" "<body>" "<p>This is some <b>simple</b> <i>formatted</i> text.</p>" "</body>" "</html>", date]; NSData *presentation = [simpleHtml dataUsingEncoding:NSUTF8StringEncoding]; NSMutableURLRequest * request = [[NSMutableURLRequest alloc] initWithURL:[[NSURL alloc] initWithString:fullEndpoint]]; request.HTTPMethod = @"POST"; request.HTTPBody = presentation; [request addValue:@"text/html" forHTTPHeaderField:@"Content-Type"]; if (self.client) { // Send the HTTP request. [self sendRequest:request]; } }
添加 formatDate 方法为 meta 标记时间戳获取 ISO 8601 格式的日期。
// Format the "created" date. OneNote requires the ISO 8601 format. - (NSString *)formatDate { NSDate *now = [NSDate date]; NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; NSLocale *enUSPOSIXLocale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; [dateFormatter setLocale:enUSPOSIXLocale]; [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZZZZ"]; return [dateFormatter stringFromDate:now]; }
创建应用后,你可以通过在 iPhone 或 iPhone 模拟器上运行应用对其进行测试。 使用你的 Microsoft 帐户或者工作或学校帐户登录。
当应用打开时,选择要在其中创建页面的分区,然后选择 “创建页面”。 然后检查 Xcode 中的输出窗口,查看日志消息。 如果调用正常,输出窗口将显示新页面的资源 URI,以及用于在 OneNote 中打开页面的链接。
后续步骤
添加功能、输入验证和错误处理。
例如,添加一个调用此方法的注销按钮:
- (IBAction)signOut:(UIButton *)sender {
[self.client signOutWithCompletion:^(NSError *signOutError) {
self.client = nil;
NSLog(@"Logged out.");
}];
}
请参阅使用 OneNote API 进行开发一文,详细了解你可使用 OneNote API 进行的操作。
适用于 iOS 的完整代码示例
AppDelegate.m
#import "AppDelegate.h"
#import <OneDriveSDK/OneDriveSDK.h>
@interface AppDelegate ()
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Set the client ID and permission scopes of your app registered on the Microsoft account Developer Center.
static NSString *const msaClientId = @"000000001A123456";
static NSString *const msaScopesString = @"wl.signin,wl.offline_access,office.onenote_update";
// Set the client ID and redirect URI of your app registered on the Azure Management Portal.
static NSString *const aadClientId = @"0b18d05c-386d-4133-b481-az1234567890";
static NSString *const aadRedirectUri = @"https://localhost/";
// Set properties on the ODClient.
NSArray *const msaScopes = [msaScopesString componentsSeparatedByString:@","];
[ODClient setMicrosoftAccountAppId:msaClientId
scopes:msaScopes];
[ODClient setActiveDirectoryAppId:aadClientId
capability:@"Notes"
redirectURL:aadRedirectUri];
return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
- (void)applicationWillTerminate:(UIApplication *)application {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
@end
ViewController.h
#import <UIKit/UIKit.h>
#import <OneDriveSDK/OneDriveSDK.h>
@interface ViewController : UIViewController<UIPickerViewDelegate, UIPickerViewDataSource>
@property (strong, nonatomic) ODClient *client;
// Variables to store the response data.
@property (strong, nonatomic) NSHTTPURLResponse *returnResponse;
@property (strong, nonatomic) NSMutableData *returnData;
// Outlet connections for controls.
@property (weak, nonatomic) IBOutlet UIPickerView *sectionPicker;
@property (weak, nonatomic) IBOutlet UIButton *createPageButton;
@end
ViewController.m
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
static NSString const *serviceRootUrl = @"https://www.onenote.com/api/v1.0/me/notes/";
NSMutableDictionary *sectionNamesAndIds;
NSArray *sectionNamesForPicker;
- (void)viewDidLoad {
[super viewDidLoad];
self.sectionPicker.delegate = self;
self.sectionPicker.dataSource = self;
[ODClient clientWithCompletion:^(ODClient *odClient, NSError *error) {
if (!error){
self.client = odClient;
[self getSections];
}
else {
NSLog(@"Error with auth: %@", [error localizedDescription]);
}
}];
}
// Build the "GET sections" request.
- (void)getSections {
// Construct the request URI and the request.
NSString *sectionsEndpoint =
[serviceRootUrl stringByAppendingString:@"sections?select=name,id&top=10&orderby=lastModifiedTime%20desc"];
NSMutableURLRequest *request =
[[NSMutableURLRequest alloc] initWithURL:[[NSURL alloc] initWithString:sectionsEndpoint]];
request.HTTPMethod = @"GET";
if (self.client)
{
// Send the HTTP request.
[self sendRequest:request];
}
_createPageButton.enabled = false;
}
// Send the request.
- (void)sendRequest:(NSMutableURLRequest *)request {
// Add the required Authorization header with access token.
[self.client.authProvider appendAuthHeaders:request completion:^(NSMutableURLRequest *requests, NSError *error) {
// This app also uses the OneDrive SDK to send HTTP requests.
[[self.client.httpProvider dataTaskWithRequest:(request)
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
[self handleResponse:data response:response error:error];
}] resume];
}];
}
// Store the section names and IDs, and populate the section picker.
- (void)getSectionNamesAndIds {
// Get the "value" array that contains the returned sections.
NSDictionary *results = [self convertData];
// Add the name-id pairs to sectionNamesAndIds, which is used to map section names to IDs.
if ([results objectForKey:@"value"] != nil) {
NSDictionary *sections =[results objectForKey:@"value"];
sectionNamesAndIds = [[NSMutableDictionary alloc] init];
for (NSMutableDictionary *dict in sections) {
NSString *sectionName = [dict objectForKey:@"name"];
NSString *sectionId = [dict objectForKey:@"id"];
sectionNamesAndIds[sectionName] = sectionId;
}
}
// Populate the picker with the section names.
sectionNamesForPicker = [sectionNamesAndIds allKeys];
dispatch_async(dispatch_get_main_queue(), ^{[_sectionPicker reloadComponent:0];});
_createPageButton.enabled = true;
}
// Get the OneNote entity data from the response.
- (NSDictionary *)convertData {
// Convert the message body to JSON.
NSError *parseError;
NSDictionary *data = [NSJSONSerialization JSONObjectWithData:self.returnData options:kNilOptions error:&parseError];
if (!parseError) {
return data;
}
else {
NSLog(@"Error parsing response: %@", [parseError localizedDescription]);
return nil;
}
}
// Handle the response.
- (void)handleResponse:(NSData *)data response:(NSURLResponse *)response error:(NSError *) error {
// Log the response.
NSLog(@"Response %@ with error %@.\n", response, error);
NSString *stringData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"Body: %@.\n", stringData);
// Store the response.
self.returnData = [[NSMutableData alloc] init];
NSMutableData *convertedData = [data mutableCopy];
[self.returnData appendData:convertedData];
self.returnResponse = (NSHTTPURLResponse *)response;
NSInteger status = [self.returnResponse statusCode];
// Check for "GET sections" success.
if (status == 200) {
NSLog(@"Sections retrieved!\n");
// Get the section data and populate the picker.
[self getSectionNamesAndIds];
}
// Check for "POST pages" success.
else if (status == 201) {
NSLog(@"Page created!\n");
// Get the page object and parse out some properties.
NSDictionary *pageProperties = [self convertData];
NSString *selfLink = [pageProperties objectForKey:@"self"];
NSDictionary *links = [pageProperties objectForKey:@"links"];
NSString *clientUrl = [[links objectForKey:@"oneNoteClientUrl"] objectForKey:@"href"];
NSString *webUrl = [[links objectForKey:@"oneNoteWebUrl"] objectForKey:@"href"];
NSLog(@"Link to new page endpoint: %@\n", selfLink);
NSLog(@"Link open page in the installed client: %@\n", clientUrl);
NSLog(@"Link to open page in OneNote Online: %@\n", webUrl);
}
else {
NSLog(@"Status code: %ld. Check the logged response for more information.", (long)status);
}
}
// Create a simple page.
- (IBAction)createPage:(id)sender {
// Get the ID of the section that's selected in the picker.
NSInteger row = [self.sectionPicker selectedRowInComponent:0];
NSString *selectedSectionName = sectionNamesForPicker[row];
NSString *selectedSectionId = sectionNamesAndIds[selectedSectionName];
// Construct the request URI and the request.
NSString *pagesEndpoint = [NSString stringWithFormat:@"sections/%@/pages", selectedSectionId];
NSString *fullEndpoint = [serviceRootUrl stringByAppendingString:pagesEndpoint];
NSString *date = [self formatDate];
NSString *simpleHtml = [NSString stringWithFormat:@"<html>"
"<head>"
"<title>A page created from simple HTML from iOS</title>"
"<meta name=\"created\" content=\"%@\" />"
"</head>"
"<body>"
"<p>This is some <b>simple</b> <i>formatted</i> text.</p>"
"</body>"
"</html>", date];
NSData *presentation = [simpleHtml dataUsingEncoding:NSUTF8StringEncoding];
NSMutableURLRequest * request = [[NSMutableURLRequest alloc] initWithURL:[[NSURL alloc] initWithString:fullEndpoint]];
request.HTTPMethod = @"POST";
request.HTTPBody = presentation;
[request addValue:@"text/html" forHTTPHeaderField:@"Content-Type"];
if (self.client)
{
// Send the HTTP request.
[self sendRequest:request];
}
}
// Format the "created" date. OneNote requires the ISO 8601 format.
- (NSString *)formatDate {
NSDate *now = [NSDate date];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
NSLocale *enUSPOSIXLocale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
[dateFormatter setLocale:enUSPOSIXLocale];
[dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZZZZ"];
return [dateFormatter stringFromDate:now];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - Delegate Methods
-(NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return 1;
}
-(NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
return sectionNamesForPicker.count;
}
-(NSString *)pickerView:(UIPickerView*)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component {
return [sectionNamesForPicker objectAtIndex:row];
}
@end
为 ASP.NET MVC 创建 OneNote 应用
此 Web 应用程序使用适用于 .NET 的 Azure Active Directory 身份验证库 (ADAL) 对多个租户中的工作和学校帐户进行身份验证。
先决条件
要执行本教程的步骤,你需要以下各项:
- Visual Studio 2015。 你可以使用免费的 Visual Studio Community 版。
- 在 Azure 管理门户上注册的 Web 应用程序,具有以下委派权限:
- 登录并读取 Windows Azure Active Directory 的用户配置文件
- 查看和修改 OneNote 的 OneNote 笔记本
备注
Visual Studio 会在创建应用时为你注册 Web 应用,但你仍然需要为 OneNote 添加权限并生成应用密钥。 (详细了解应用注册。)
使用 ASP.NET MVC 创建 OneNote 应用:
教程结尾提供了关键示例文件的完整代码示例。
在 Visual Studio 中创建项目
在 Visual Studio 中,创建一个名为OneNote-WebApp 的 ASP.NET Web 应用程序项目。
选择 “MVC” 模板,并确保为 “添加文件夹和核心参考选项选择“MVC”。
选择 “更改身份验证”,然后选择 “工作和学校帐户”。
选择 “云 - 多个组织”,并输入开发者租户的域名(例如,contoso.onmicrosoft.com)。
你可以根据需要保留或清除 Microsoft Azure 云主机设置。 本教程无需执行该操作。 保留所有其他默认设置。
Visual Studio 将为你向 Azure 注册 Web 应用,但你需要在 Azure 管理门户中完成其配置。
在门户的租户目录中,选择 “应用”,然后单击 “OneNote-Web” 应用打开其配置页面。
在 “密钥” 部分,选择新密钥的持续时间。
在 “针对其他应用程序的权限” 部分,添加 OneNote 应用程序,然后添加 “查看并修改 OneNote 笔记本” 委派权限。 ( 了解更多)
保存对应用所做的更改,并在关闭门户之前复制新密钥。 你很快将用到该密钥。
添加适用于 .NET 的 ADAL
应用使用 Active Directory 身份验证库 (ADAL) 对 Azure AD 进行身份验证和授权。 该应用使用版本 2.19.208020213 创建。
在 Visual Studio 中,选择 “工具”>“NuGet 包管理器” >“包管理器控制台,并在控制台中运行以下命令。
Install-Package Microsoft.IdentityModel.Clients.ActiveDirectory
构建 UI
此应用为 HomeController 使用两个视图:Index.cshtml和Page.cshtml。
用下面的代码替换 Views/Home/Index.cshtml 的内容。 这会添加一个用于选择父分区的下拉列表、一个用于输入页面名称的文本框和一个按钮。
@model OneNote_WebApp.Models.SectionsViewModel @{ ViewBag.Title = "Index"; } <h2>OneNote ASP.NET MVC web application</h2> @Html.Label("Choose a section to create the page in.") @using (Html.BeginForm("CreatePageAsync", "Home", new AjaxOptions { UpdateTargetId = "create-page" })) { <div id="create-page"> @Html.DropDownListFor( m => m.SectionId, new SelectList(Model.Sections, "Id", "Name", Model.SectionId)) @Html.ValidationMessageFor(m => m.SectionId) <br /> <br /> <table> <tr> <td> @Html.Label("Enter a name for the new page.") <br /> @Html.TextBox("page-name", null, new { @style = "width=80" }) </td> </tr> </table> <button>Create page</button> </div> }
在 Views/Home 文件夹中,创建一个名为 “Page” 的新视图,并添加以下代码。 此视图显示新创建的页面的属性。
@model OneNote_WebApp.Models.PageViewModel @{ ViewBag.Title = "Page"; } <h2>Page: @Model.Title</h2> <table> <tr> <td>@Html.Label("Self link: ")</td> <td>@Model.Self</td> </tr> <tr> <td>@Html.Label("Native client link: ")</td> <td><a href="@Model.PageLinks.ClientUrl">@Model.PageLinks.ClientUrl</a></td> </tr> <tr> <td>@Html.Label("Web client link: ")</td> <td><a href="@Model.PageLinks.WebUrl">@Model.PageLinks.WebUrl</a></td> </tr> </table>
添加身份验证支持
适用于.NET 的 ADAL 客户端库处理身份验证和授权过程。 你只需为你的应用程序提供标识符并添加几个调用即可。
在根 Web.config 文件中,将以下键/值对添加到 appSettings 节点。 请注意,Visual Studio 已添加了 ClientId 和 AADInstance。
<add key="ida:AppKey" value="ENTER-your-app-key-here" /> <add key="ida:OneNoteResourceId" value="https://onenote.com/" />
用你之前生成的密钥替换应用密钥的占位符值。
在 App_Start/Start.Auth.cs 中,添加以下 using 语句。
using Microsoft.IdentityModel.Clients.ActiveDirectory;
将 Startup 类中的全局变量替换为以下代码。 HomeController 中的 GetAuthorizedClient 方法也使用四个公共变量。
public static string ClientId = ConfigurationManager.AppSettings["ida:ClientId"]; public static string AppKey = ConfigurationManager.AppSettings["ida:AppKey"]; public static string AADInstance = ConfigurationManager.AppSettings["ida:AADInstance"]; public static string OneNoteResourceId = ConfigurationManager.AppSettings["ida:OneNoteResourceId"]; private string Authority = AADInstance + "common";
在 ConfigureAuth 方法中,将 app.UseOpenIdConnectAuthentication 方法替换为以下代码。 ADAL 将令牌和其他信息存储在令牌缓存中。 (要查看缓存的内容,请在返回任务之前添加以下行:
var cache = context.TokenCache.ReadItems();
)app.UseOpenIdConnectAuthentication( new OpenIdConnectAuthenticationOptions { ClientId = ClientId, Authority = Authority, TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters { ValidateIssuer = false }, Notifications = new OpenIdConnectAuthenticationNotifications() { AuthorizationCodeReceived = (context) => { var code = context.Code; ClientCredential credential = new ClientCredential(ClientId, AppKey); Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext authContext = new Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext(Authority); AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode( code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, OneNoteResourceId ); return Task.FromResult(0); }, AuthenticationFailed = (context) => { context.HandleResponse(); if (context.Exception.HResult == -2146233088) //IDX10311: Nonce is null { context.OwinContext.Response.Redirect("Home/Index"); } return Task.FromResult(0); } } });
在 Controllers/HomeController.cs 中,添加以下 using 语句。
using Microsoft.IdentityModel.Clients.ActiveDirectory; using Microsoft.Owin.Security; using Microsoft.Owin.Security.OpenIdConnect; using Newtonsoft.Json; using System.IO; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Claims; using System.Text; using System.Threading.Tasks; using OneNote_WebApp.Models;
在 **HomeController ** 类中,添加 GetAuthorizedClient 方法。 此方法创建并配置用于向 OneNote 服务发出 REST 请求的 HttpClient。 它还获取访问令牌并将其添加到客户端。
private HttpClient GetAuthorizedClient() { HttpClient client = new HttpClient(); string userObjectId = ClaimsPrincipal.Current.FindFirst("https://schemas.microsoft.com/identity/claims/objectidentifier").Value; string tenantId = ClaimsPrincipal.Current.FindFirst("https://schemas.microsoft.com/identity/claims/tenantid").Value; ClientCredential credential = new ClientCredential(Startup.ClientId, Startup.AppKey); AuthenticationContext authContext = new AuthenticationContext(Startup.AADInstance + tenantId); try { // Call AcquireTokenSilent to get the access token. This first tries to get the token from cache. AuthenticationResult authResult = authContext.AcquireTokenSilent( Startup.OneNoteResourceId, credential, new UserIdentifier(userObjectId, UserIdentifierType.UniqueId)); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); } catch (AdalSilentTokenAcquisitionException) { HttpContext.GetOwinContext().Authentication.Challenge( new AuthenticationProperties() { RedirectUri = "/" }, OpenIdConnectAuthenticationDefaults.AuthenticationType); return null; } return client; }
现在你已准备好调用 OneNote 服务并解析响应。
调用 OneNote API
当应用加载时,它会获取 10 个最近修改的分区的名称和 ID,并使用分区名称填充下拉列表。 将在选定分区中创建新 OneNote 页面。
在 HomeController 类中,添加表示 OneNote 端点和图像文件路径的全局变量,以添加到新页面。
public static string OneNoteRoot = "https://www.onenote.com/api/v1.0/me/notes/"; public static string SectionsEndpoint = "sections?select=name,id&top=10&orderby=lastModifiedTime%20desc"; public static string PagesEndpoint = "sections/{0}/pages"; public static string PathToImageFile = @"C:\<local-path>\logo.png";
将 PathToImageFile 变量中的占位符路径和文件名更改为指向一个本地 PNG 图像。
将 Index 方法替换为以下代码。 这将获取分区,为索引视图准备 SectionsViewModel,并加载视图。
public async Task<ActionResult> Index() { SectionsViewModel viewModel = new SectionsViewModel(); try { viewModel.Sections = await GetSectionsAsync(); } catch (Exception ex) { return View("Error", new HandleErrorInfo(new Exception(ex.Message), "Home", "GetSectionsAsync")); } return View(viewModel); }
添加 GetSectionsAsync 方法来构建和发送 GET sections 请求并解析响应。
[Authorize] [HttpGet] public async Task<IEnumerable<Section>> GetSectionsAsync() { List<Section> sections = new List<Section>(); HttpClient client = GetAuthorizedClient(); HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, OneNoteRoot + SectionsEndpoint); HttpResponseMessage response = await client.SendAsync(request); if (response.IsSuccessStatusCode) { // Parse the JSON response. string stringResult = await response.Content.ReadAsStringAsync(); Dictionary<string, dynamic> result = JsonConvert.DeserializeObject<Dictionary<string, dynamic>>(stringResult); foreach (var item in result["value"]) { var current = item.ToObject<Dictionary<string, string>>(); Section section = new Section { Name = current["name"], Id = current["id"] }; sections.Add(section); } } else { throw new Exception("Error getting sections: " + response.StatusCode.ToString()); } return sections; }
添加 CreatePageAsync 方法来构建和发送 POST pages 多部分请求并解析响应。 此请求将创建一个简单的 HTML 页面。
[Authorize] [HttpPost] public async Task<ActionResult> CreatePageAsync() { HttpClient client = GetAuthorizedClient(); // Get user input. string selectedSectionId = Request.Form["SectionId"]; string pageName = Request.Form["page-name"]; string pagesEndpoint = string.Format("sections/{0}/pages", selectedSectionId); // Define the page content, which includes an uploaded image. const string imagePartName = "imageBlock1"; string iso8601Date = DateTime.Now.ToString("o"); string pageHtml = "<html>" + "<head>" + "<title>" + pageName + "</title>" + "<meta name=\"created\" content=\"" + iso8601Date + "\" />" + "</head>" + "<body>" + "<h1>This is a page with an image</h1>" + "<img src=\"name:" + imagePartName + "\" alt=\"No mis monos\" width=\"250\" height=\"200\" />" + "</body>" + "</html>"; HttpResponseMessage response; // Build the 'POST pages' request. var stream = new FileStream(PathToImageFile, FileMode.Open); using (var imageContent = new StreamContent(stream)) { try { imageContent.Headers.ContentType = new MediaTypeHeaderValue("image/png"); MultipartFormDataContent pageContent = new MultipartFormDataContent { {new StringContent(pageHtml, Encoding.UTF8, "text/html"), "Presentation"}, {imageContent, imagePartName} }; response = await client.PostAsync(OneNoteRoot + pagesEndpoint, pageContent); if (!response.IsSuccessStatusCode) { throw new Exception(response.StatusCode + ": " + response.ReasonPhrase); } else { // Parse the JSON response. string stringResult = await response.Content.ReadAsStringAsync(); Dictionary<string, dynamic> pageData = JsonConvert.DeserializeObject<Dictionary<string, dynamic>>(stringResult); Dictionary<string, dynamic> linksData = JsonConvert.DeserializeObject<Dictionary<string, dynamic>>(pageData["links"].ToString()); Links pageLinks = new Links { ClientUrl = new Uri(linksData["oneNoteClientUrl"]["href"].ToString()), WebUrl = new Uri(linksData["oneNoteWebUrl"]["href"].ToString()) }; PageViewModel pageViewModel = new PageViewModel { Title = pageData["title"], Self = new Uri(pageData["self"]), PageLinks = pageLinks }; return View("../home/page", pageViewModel); } } catch (Exception ex) { return View("Error", new HandleErrorInfo(new Exception(ex.Message), "Home", "CreatePageAsync")); } } }
在 Models 文件夹中,添加名为 Resource.cs 的新类,并使用以下代码。 这定义了表示 OneNote 分区和页面的域模型,以及在 “索引” 和 “页面” 视图中表示 OneNote 数据的视图模型。
using System; using System.Collections.Generic; using System.IO; namespace OneNote_WebApp.Models { // Common properties of OneNote entities. public class Resource { public string Id { get; set; } public string CreatedBy { get; set; } public DateTimeOffset CreatedTime { get; set; } public string LastModifiedBy { get; set; } public DateTimeOffset LastModifiedTime { get; set; } public Uri Self { get; set; } } // A OneNote section with some key properties. public class Section : Resource { public bool IsDefault { get; set; } public string Name { get; set; } public ICollection<Page> Pages { get; set; } public Uri PagesUrl { get; set; } } // A OneNote page with some key properties. // This app doesn't use the Page model. public class Page : Resource { public Stream Content { get; set; } public Uri ContentUrl { get; set; } public Links PageLinks { get; set; } public string Title { get; set; } } // The links that open a OneNote page in the installed client or in OneNote Online. public class Links { public Uri ClientUrl { get; set; } public Uri WebUrl { get; set; } } // The view model used to populate the section drop-down list. public class SectionsViewModel { public string SectionId { get; set; } public IEnumerable<Section> Sections { get; set; } } // The view model used to display properties of the new page. public class PageViewModel { public string Title { get; set; } public Uri Self { get; set; } public Links PageLinks { get; set; } } }
创建应用后,你可以使用 F5 调试来运行它。
如果出现 “找不到包含 OwinStartupAttribute 的程序集...” 错误,请在根目录内 Startup.cs 类中的 using 语句后添加以下属性。 (详细了解此错误。)
[assembly: OwinStartup(typeof(OneNote_WebApp.Startup))]
使用至少有一个笔记本(其中包含一个分区)的工作或学校帐户登录应用。 在应用中,选择要在其中创建页面的分区,为新页面输入名称,然后选择 “创建页面”。 如果成功,应用将显示标题及自身,以及新页面的页面链接。
ASP.NET MVC 的完整代码示例
Startup.Auth.cs
using System;
using System.Configuration;
using System.Threading.Tasks;
using System.Web;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
namespace OneNote_WebApp
{
public partial class Startup
{
// Properties used for authorization.
public static string ClientId = ConfigurationManager.AppSettings["ida:ClientId"];
public static string AppKey = ConfigurationManager.AppSettings["ida:AppKey"];
public static string AADInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
public static string OneNoteResourceId = ConfigurationManager.AppSettings["ida:OneNoteResourceId"];
private string Authority = AADInstance + "common";
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions { });
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = ClientId,
Authority = Authority,
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = false
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(ClientId, AppKey);
Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext authContext =
new Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext(Authority);
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
code,
new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)),
credential,
OneNoteResourceId
);
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
context.HandleResponse();
if (context.Exception.HResult == -2146233088) //IDX10311: Nonce is null
{
context.OwinContext.Response.Redirect("Home/Index");
}
return Task.FromResult(0);
}
}
});
}
}
}
HomeController
using System;
using System.Collections.Generic;
using System.Web.Mvc;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.OpenIdConnect;
using Newtonsoft.Json;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using OneNote_WebApp.Models;
namespace OneNote_WebApp.Controllers
{
[Authorize]
public class HomeController : Controller
{
// Route segments to OneNote resource endpoints.
public static string OneNoteRoot = "https://www.onenote.com/api/v1.0/me/notes/";
public static string SectionsEndpoint = "sections?select=name,id&top=10&orderby=lastModifiedTime%20desc";
public static string PagesEndpoint = "sections/{0}/pages";
// Path to the image file to add to the page.
// Change this to point to a local PNG file before running the app.
public static string PathToImageFile = @"C:\<local-path>\logo.png";
// Get sections, add them to SectionsViewModel, and load the view.
public async Task<ActionResult> Index()
{
SectionsViewModel viewModel = new SectionsViewModel();
try
{
viewModel.Sections = await GetSectionsAsync();
}
catch (Exception ex)
{
return View("Error", new HandleErrorInfo(new Exception(ex.Message), "Home", "GetSectionsAsync"));
}
return View(viewModel);
}
// Create and configure the HttpClient used for requests to the OneNote API.
private HttpClient GetAuthorizedClient()
{
HttpClient client = new HttpClient();
string userObjectId = ClaimsPrincipal.Current.FindFirst("https://schemas.microsoft.com/identity/claims/objectidentifier").Value;
string tenantId = ClaimsPrincipal.Current.FindFirst("https://schemas.microsoft.com/identity/claims/tenantid").Value;
ClientCredential credential = new ClientCredential(Startup.ClientId, Startup.AppKey);
AuthenticationContext authContext = new AuthenticationContext(Startup.AADInstance + tenantId);
try
{
// Call AcquireTokenSilent to get the access token. This first tries to get the token from cache.
AuthenticationResult authResult = authContext.AcquireTokenSilent(
Startup.OneNoteResourceId,
credential,
new UserIdentifier(userObjectId, UserIdentifierType.UniqueId));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
catch (AdalSilentTokenAcquisitionException)
{
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties() { RedirectUri = "/" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
return null;
}
return client;
}
[Authorize]
[HttpGet]
// Build the 'GET sections' request and parse the response. The request gets the 10 most recently modified sections.
public async Task<IEnumerable<Section>> GetSectionsAsync()
{
List<Section> sections = new List<Section>();
HttpClient client = GetAuthorizedClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, OneNoteRoot + SectionsEndpoint);
HttpResponseMessage response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
// Parse the JSON response.
string stringResult = await response.Content.ReadAsStringAsync();
Dictionary<string, dynamic> result = JsonConvert.DeserializeObject<Dictionary<string, dynamic>>(stringResult);
foreach (var item in result["value"])
{
var current = item.ToObject<Dictionary<string, string>>();
Section section = new Section
{
Name = current["name"],
Id = current["id"]
};
sections.Add(section);
}
}
else
{
throw new Exception("Error getting sections: " + response.StatusCode.ToString());
}
return sections;
}
[Authorize]
[HttpPost]
// Build the multipart POST request and parse the response. The request creates a page in the selected section.
public async Task<ActionResult> CreatePageAsync()
{
HttpClient client = GetAuthorizedClient();
// Get user input.
string selectedSectionId = Request.Form["SectionId"];
string pageName = Request.Form["page-name"];
string pagesEndpoint = string.Format("sections/{0}/pages", selectedSectionId);
// Define the page content, which includes an uploaded image.
const string imagePartName = "imageBlock1";
string iso8601Date = DateTime.Now.ToString("o");
string pageHtml = "<html>" +
"<head>" +
"<title>" + pageName + "</title>" +
"<meta name=\"created\" content=\"" + iso8601Date + "\" />" +
"</head>" +
"<body>" +
"<h1>This is a page with an image</h1>" +
"<img src=\"name:" + imagePartName +
"\" alt=\"No mis monos\" width=\"250\" height=\"200\" />" +
"</body>" +
"</html>";
HttpResponseMessage response;
// Build the 'POST pages' request.
var stream = new FileStream(PathToImageFile, FileMode.Open);
using (var imageContent = new StreamContent(stream))
{
try
{
imageContent.Headers.ContentType = new MediaTypeHeaderValue("image/png");
MultipartFormDataContent pageContent = new MultipartFormDataContent
{
{new StringContent(pageHtml, Encoding.UTF8, "text/html"), "Presentation"},
{imageContent, imagePartName}
};
response = await client.PostAsync(OneNoteRoot + pagesEndpoint, pageContent);
if (!response.IsSuccessStatusCode)
{
throw new Exception(response.StatusCode + ": " + response.ReasonPhrase);
}
else
{
// Parse the JSON response.
string stringResult = await response.Content.ReadAsStringAsync();
Dictionary<string, dynamic> pageData = JsonConvert.DeserializeObject<Dictionary<string, dynamic>>(stringResult);
Dictionary<string, dynamic> linksData = JsonConvert.DeserializeObject<Dictionary<string, dynamic>>(pageData["links"].ToString());
Links pageLinks = new Links
{
ClientUrl = new Uri(linksData["oneNoteClientUrl"]["href"].ToString()),
WebUrl = new Uri(linksData["oneNoteWebUrl"]["href"].ToString())
};
PageViewModel pageViewModel = new PageViewModel
{
Title = pageData["title"],
Self = new Uri(pageData["self"]),
PageLinks = pageLinks
};
return View("../home/page", pageViewModel);
}
}
catch (Exception ex)
{
return View("Error", new HandleErrorInfo(new Exception(ex.Message), "Home", "CreatePageAsync"));
}
}
}
public ActionResult About()
{
ViewBag.Message = "Your application description page.";
return View();
}
public ActionResult Contact()
{
ViewBag.Message = "Your contact page.";
return View();
}
}
}
Index.cshtml、Page.cshtml 和 Resource.cs 在说明中完整显示。