Protips for Windows Azure Mobile Services

Preview:

DESCRIPTION

Protips for Windows Azure Mobile Services . Chris Risner Technical Evangelist 3-543. Introduction. Windows Azure Technical Evangelist. @ chrisrisner. Mobile Developer. http:// chrisrisner.com. Former .NET developer. Live in Washington. Grew up in Michigan. - PowerPoint PPT Presentation

Citation preview

Protips for Windows Azure Mobile Services Chris RisnerTechnical Evangelist3-543

Introduction

@chrisrisner

http://chrisrisner.com

Live in Washington

Windows Azure Technical Evangelist

Mobile Developer

Former .NET developer

Co-Organizer of Seattle GDG

Grew up in Michigan

Agenda

Mobile Services Recap

Tricks

Tips

Tips

Tricks Questions

Mobile Services Recap

Windows Azure Mobile Services

Data

Notifications

Auth

Server Logic

Scheduler

Logging & Diag

Scale

Multi-Platform Apps

You don’t need a different Mobile Service for each platform!

Connect them all!

Cross-Platform Support

Cross-Platform Support

Multi-Device Push

Single Platform Push NotificationsWindows Storepush.wns.sendToastText04(item.channel, {text1: text}, … );

Windows Phonepush.mpns.sendFlipTile(item.channel, {title: text}, …);

iOSpush.apns.send(item.token, { alert: text, payload:

{ inAppMessage: Details }}, …);

Androidpush.gcm.send(item.registrationId, item.text, …);

Multi-Platform Push Notificationsfunction sendNotifications() {

var deviceInfoTable = tables.getTable('DeviceInfo');

deviceInfoTable.where({ userId : user.userId }).read({

success: function(deviceInfos){

deviceInfos.forEach(function(deviceInfo){

if (deviceInfo.uuid != request.parameters.uuid) {

if (deviceInfo.pushToken != null && deviceInfo.pushToken != 'SimulatorToken') {

if (deviceInfo.platform == 'iOS') {

push.apns.send(deviceInfo.pushToken, {

alert: "New something created"

} , { //success / error block});

} else if (deviceInfo.platform == 'Android') {

push.gcm.send(deviceInfo.pushToken, "New something created", { success / error block});

}

}

}

});

}

});

}

Don’t forget to check the response on error (or getFeedback for APNS)

Also, check out Delivering Push Notifications to Millions of Devices – Friday @12pm

Virtual Tables

Create a tableUse it’s endpointDon’t call request.Execute

Custom API

• Non-table based scripts• Accessible from• GET• POST• PUT• PATCH• DELETE

• Permissions based

Custom API

Custom API Demo

Talking to Azure Storage

It’s doableIt’s not perfectScriptsand the Azure module

Reading Tablesvar azure = require('azure');

function read(query, user, request) {

var accountName = 'accountname';

var accountKey = 'Accountkey------------nKHDsW2/0Jzg==';

var host = accountName + '.table.core.windows.net';

var tableService = azure.createTableService(accountName, accountKey, host);

tableService.queryTables(function (error, tables) {

if (error) {

request.respond(500, error);

} else {

request.respond(200, tables);

}

});

}

Reading Table Rowsvar azure = require('azure');

function read(query, user, request) {

var accountName = 'accountname';

var accountKey = 'Accountkey------------nKHDsW2/0Jzg==';

var host = accountName + '.table.core.windows.net';

var tableService = azure.createTableService(accountName, accountKey, host);

var tq = azure.TableQuery

.select()

.from(request.parameters.table);

tableService.queryEntities(tq, function (error, rows) {

if (error) {

request.respond(500, error);

} else {

request.respond(200, rows)

}

});

}

Creating Containersvar azure = require('azure');

function insert(item, user, request) {

var accountName = 'accountname';

var accountKey = 'Accountkey------------nKHDsW2/0Jzg==';

var host = accountName + '.blob.core.windows.net';

var blobService = azure.createBlobService(accountName, accountKey, host);

if (request.parameters.isPublic == 1) {

blobService.createContainerIfNotExists(item.containerName

,{publicAccessLevel : 'blob'}

, function (error) {

if (!error) { request.respond(200, item); } else { /* error */ request.respond(500);}

});

} else {

blobService.createContainerIfNotExists(item.containerName, function (error) {

if (!error) { request.respond(200, item); } else { /*error */ request.respond(500);

}

});

}

}

Reading and “Creating” Blobsvar azure = require('azure'), qs = require('querystring');

function insert(item, user, request) {

var accountName = 'accountname';

var accountKey = 'Accountkey------------nKHDsW2/0Jzg==';

var host = accountName + '.blob.core.windows.net';

var blobService = azure.createBlobService(accountName, accountKey, host);

var sharedAccessPolicy = {

AccessPolicy: {

Permissions: 'rw', //Read and Write permissions

Expiry: minutesFromNow(5)

}

};

var sasUrl = blobService.generateSharedAccessSignature(request.parameters.containerName,

request.parameters.blobName, sharedAccessPolicy);

var sasQueryString = { 'sasUrl' : sasUrl.baseUrl + sasUrl.path + '?' + qs.stringify(sasUrl.queryString) };

request.respond(200, sasQueryString);

}

function minutesFromNow(minutes) {

var date = new Date()

date.setMinutes(date.getMinutes() + minutes);

return date;

}

Storage Demo

Talking REST

The REST API

Action HTTP Verb URL SuffixCreate POST /TodoItemRetrieve GET /TodoItem?$filter=id%3D42Update PATCH /TodoItem/idDelete DELETE /TodoItem/id

Data Operations and their REST Equivalents

Base REST API Endpoint URLhttp://Mobileservice.azure-mobile.net/tables/*

JSON to SQL Type MappingsJSON Value T-SQL TypeNumeric values (integer, decimal, floating point)

Float

Boolean BitDateTime DateTimeOffset(3)String Nvarchar(max)

Postman &Runscope Demo

Sending Emails

Sending an Email//var crypto = require('crypto');

//item.tempId = new Buffer(crypto.randomBytes(16)).toString('hex');

function sendEmail(item) {

var sendgrid = new SendGrid('myaccount@azure.com', 'mypassword');

var email = {

to : item.email,

from : 'from-me@chrisrisner.com',

subject : 'Welcome to MyApp',

text: 'Thanks for installing My App! Click this link to verify:\n\n'

+ 'http://myapp.azurewebsites.net/activate.html?id=' + item.id + '&tid=' + item.tempId,

createDate : new Date()

};

sendgrid.send({

to: item.email,

from: email.from,

subject: email.subject,

text: email.text

}, function(success, message) {

// If the email failed to send, log it as an error so we can investigate

if (!success) {

console.error(message);

} else {

saveSentEmail(email);

}

});

}

Setting up SendGrid Demo

The CLI

It’s aweSOME

CLI Demo

Service Filters and DelegatingHandlers

Client sideIntercepts requestsIntercepts responses

Sending Version Info with Each Request- (void)handleRequest:(NSURLRequest *)request next:(MSFilterNextBlock)next response:(MSFilterResponseBlock)response{ MSFilterResponseBlock wrappedResponse = ^(NSHTTPURLResponse *innerResponse, NSData *data, NSError *error) { response(innerResponse, data, error); }; // add additional versioning information to the querystring for versioning purposes NSString *append = [NSString stringWithFormat:@"build=%@&version=%@", self.build, self.version]; NSURL *url = nil; NSRange range = [request.URL.absoluteString rangeOfString:@"?"]; if (range.length > 0) { url = [NSURL URLWithString:[NSString stringWithFormat:@"%@&%@&p=iOS", request.URL.absoluteString, append]]; } else { url = [NSURL URLWithString:[NSString stringWithFormat:@"%@?%@&p=iOS", request.URL.absoluteString, append]]; } NSMutableURLRequest *newRequest = [request mutableCopy]; newRequest.URL = url; next(newRequest, wrappedResponse);}

DelegatingHandlers are Service Filterspublic static MobileServiceClient MobileService = new MobileServiceClient( "https://<your subdomain>.azure-mobile.net/", "<your app key>", new VersionHandler()); using System;using System.Net.Http;using System.Threading.Tasks;namespace WindowsStore{ public class VersionHandler : DelegatingHandler { protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,

System.Threading.CancellationToken cancellationToken) { request.RequestUri = new Uri(request.RequestUri.AbsoluteUri.ToString() + "?version=v2"); return base.SendAsync(request, cancellationToken); } }}

Script Versioning

Checking the Version in Scriptsfunction insert(item, user, request) { if (request.parameters.build < 2.0) { item.description = 'Not entered'; } request.execute({ success : function() { if (request.parameters.build < 2.0) { delete item.description; } request.respond(); } }); }

For more on versioning, check outGoing Live and Beyond with Windows Azure Mobile ServicesFriday @ 10:30 am

Talking Twitter

v1 is deadv1.1 is hard

Part 1: The Helpersfunction generateOAuthSignature(method, url, data){

var index = url.indexOf('?');

if (index > 0)

url = url.substring(0, url.indexOf('?'));

var signingToken = encodeURIComponent('Your Consumer Secret') + "&" + encodeURIComponent('Your Access Token Secret');

var keys = [];

for (var d in data){

if (d != 'oauth_signature') {

console.log('data: ' , d);

keys.push(d);

}

}

keys.sort();

var output = "GET&" + encodeURIComponent(url) + "&";

var params = "";

keys.forEach(function(k){

params += "&" + encodeURIComponent(k) + "=" + encodeURIComponent(data[k]);

});

params = encodeURIComponent(params.substring(1));

return hashString(signingToken, output+params, "base64");

}

function hashString(key, str, encoding){

var hmac = crypto.createHmac("sha1", key);

hmac.update(str);

return hmac.digest(encoding);

}

function generateNonce() {

var code = "";

for (var i = 0; i < 20; i++) {

code += Math.floor(Math.random() * 9).toString();

}

return code;

}

Part 2: The Work (part 1)var crypto = require('crypto');

var querystring = require('querystring');

function read(query, user, request) {

var result = {

id: query.id,

identities: user.getIdentities(),

userName: ''

};

var identities = user.getIdentities();

var userId = user.userId;

var twitterId = userId.substring(userId.indexOf(':') + 1);

//API 1.0

//url = 'https://api.twitter.com/1/users/show/' + twitterId + '.json';

//API 1.1

var url = 'https://api.twitter.com/1.1/users/show.json?user_id=' + twitterId;

var key = 'This is your consumer key';

var nonce = generateNonce();

var sigmethod = 'HMAC-SHA1';

var version = '1.0';

var twitterAccessToken = identities.twitter.accessToken;

var oauth_token = 'The Access Token';

var seconds = new Date() / 1000;

seconds = Math.round(seconds);

var requestType = 'GET';

var oauthData = { oauth_consumer_key: key, oauth_nonce: nonce, oauth_signature:null,

oauth_signature_method: sigmethod, oauth_timestamp: seconds,

oauth_token: oauth_token, oauth_version: version };

var sigData = {};

for (var k in oauthData){

sigData[k] = oauthData[k];

}

sigData['user_id'] = twitterId;

Part 2.2: The Workvar sig = generateOAuthSignature('GET', url, sigData);

oauthData.oauth_signature = sig;

var oauthHeader = "";

for (k in oauthData){

oauthHeader += ", " + encodeURIComponent(k) + "=\"" + encodeURIComponent(oauthData[k]) + "\"";

}

oauthHeader = oauthHeader.substring(1);

var authHeader = 'OAuth' + oauthHeader;

//Generate callback for response from Twitter API

var requestCallback = function (err, resp, body) {

if (err || resp.statusCode !== 200) {

console.error('Error sending data to the provider: ', err);

request.respond(statusCodes.INTERNAL_SERVER_ERROR, body);

} else {

try {

var userData = JSON.parse(body);

if (userData.name != null)

result.UserName = userData.name;

else

result.UserName = "can't get username";

request.respond(200, [result]);

} catch (ex) {

console.error('Error parsing response from the provider API: ', ex);

request.respond(statusCodes.INTERNAL_SERVER_ERROR, ex);

}

}

}

//Create the request and execute it

var req = require('request');

var reqOptions = {

uri: url,

headers: { Accept: "application/json" }

};

if (authHeader != null)

reqOptions.headers['Authorization'] = authHeader;

req(reqOptions, requestCallback);

}

That was terribleDo this

The Easy Wayexports.post = function(request, response) { var twitter = require(‘ctwitter.js’); twitter.init(’consumer key',’consumer secret'); twitter.tweet(request.body.tweettext, request.user, request);}

Get the script here: http://bit.ly/14b73Gg

Script Source Control

Enable on dashboardCreates Git repoChanges push from client

Shared Scripts

require(‘jsfile.js');

*Need a config change on update (for now)

Auth Part 1: Custom

Pass creds inValidateHash your saltCreate a JWT

Part 1: The Helpersfunction hash(text, salt, callback) { crypto.pbkdf2(text, salt, iterations, bytes, function(err, derivedKey){ if (err) { callback(err); } else { var h = new Buffer(derivedKey).toString('base64'); callback(null, h); } });} function slowEquals(a, b) { var diff = a.length ^ b.length; for (var i = 0; i < a.length && i < b.length; i++) { diff |= (a[i] ^ b[i]); } return diff === 0;} function zumoJwt(expiryDate, aud, userId, masterKey) { var crypto = require('crypto'); function base64(input) { return new Buffer(input, 'utf8').toString('base64'); } function urlFriendly(b64) { return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(new RegExp("=", "g"), ''); } function signature(input) { var key = crypto.createHash('sha256').update(masterKey + "JWTSig").digest('binary'); var str = crypto.createHmac('sha256', key).update(input).digest('base64'); return urlFriendly(str); } var s1 = '{"alg":"HS256","typ":"JWT","kid":0}'; var j2 = { "exp":expiryDate.valueOf() / 1000, "iss":"urn:microsoft:windows-azure:zumo”, "ver":1, "aud":aud, "uid":userId }; var s2 = JSON.stringify(j2); var b1 = urlFriendly(base64(s1)); var b2 = urlFriendly(base64(s2)); var b3 = signature(b1 + "." + b2); return [b1,b2,b3].join(".");}

Part 2: The Workvar crypto = require('crypto'), iterations = 1000, bytes = 32;var aud = "Custom", masterKey = "MyMobileServiceMasterKey”;function insert(item, user, request) { var accounts = tables.getTable('accounts'); if (!item.username.match(/^[a-zA-Z0-9]{5,}$/)) { request.respond(400, "Invalid username (at least 4 chars, alphanumeric only)"); return; } else if (item.password.length < 7) { request.respond(400, "Invalid password (least 7 chars required)"); return; } accounts.where({ username : item.username}).read({ success: function(results) { if (results.length > 0) { request.respond(400, "Username already exists"); return; } else { // add a unique salt to the item item.salt = new Buffer(crypto.randomBytes(bytes)).toString('base64'); // hash the password hash(item.password, item.salt, function(err, h) { item.password = h; request.execute({ success: function () { // We don't want the salt or the password going back to the client delete item.password; delete item.salt; var userId = aud + ":" + item.id; item.userId = userId; var expiry = new Date().setUTCDate(new Date().getUTCDate() + 30); item.token = zumoJwt(expiry, aud, userId, masterKey); request.respond(); } }); }); } } });}

Part 3: Signing Invar crypto = require('crypto'), iterations = 1000, bytes = 32;var aud = "Custom", masterKey = "MyMobileServiceMasterKey"; function insert(item, user, request) { var accounts = tables.getTable('accounts'); accounts.where({ username : item.username }).read({ success: function(results) { if (results.length === 0) { request.respond(401, "Incorrect username or password"); } else { var account = results[0]; hash(item.password, account.salt, function(err, h) { var incoming = h; if (slowEquals(incoming, account.password)) { var expiry = new Date().setUTCDate(new Date().getUTCDate() + 30); var userId = aud + ":" + account.id; request.respond(200, { userId: userId, token: zumoJwt(expiry, aud, userId, masterKey) }); } else { request.respond(401, "Incorrect username or password"); } }); } } });}

…or just use Auth0http://aka.ms/authZeroZumo

Check out Who’s that User? – Friday @ 2pm

Auth Part 2: Identity Caching

Storing Credentials in .NETpublic static class CredentialLocker    {        private const string RESOURCE= "MobileServices";        public static void AddCredential(string username, string password) {            var vault = new PasswordVault();            var credential = new PasswordCredential(RESOURCE, username, password);            vault.Add(credential);        }        public static PasswordCredential GetCredential() {                        PasswordCredential credential = null;                        var vault = new PasswordVault();                        try {                                        credential = vault.FindAllByResource(RESOURCE).FirstOrDefault();                                if (credential != null){                                                               credential.Password = vault.Retrieve(RESOURCE, credential.UserName).Password;                        }                }    

catch (Exception)    { //creds not found       }

            return credential;        }

        public static void RemoveCredential(string username) {                var vault = new PasswordVault();                try {                        // Removes the credential from the password vault.                        vault.Remove(vault.Retrieve(RESOURCE, username));                }                catch (Exception)    { //creds not stored       }        }    }

Getting and Setting CredentialsSetting:

MoblieServiceUser user;user = await App.MobileService.LoginAsync(MobileServiceAuthenticationProvider.Facebook);CredentialLocker.AddCredential(user.userId, user.MobileServiceAuthenticationToken);

Getting:

var credential = CredentialLocker.GetCredential();if (credential != null){ MobileService.CurrentUser = new MobileServiceUser(credential.UserName); MobileService.CurrentUser.MobileServiceAuthenticationToken =

credential.Password; }

Auth Part 3: Expired Tokens

Expiration FlowInitial request

Check response for 401

Relogin user

Update request with new token

Resend request

Update UI

DelegationHandlers (again)public class VersionHandler : DelegatingHandler{ protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,

System.Threading.CancellationToken cancellationToken) { var response = await base.SendAsync(request, cancellationToken); while (response.StatusCode == HttpStatusCode.Unauthorized) { try { await App.MobileService.LoginAsync(MobileServiceAuthenticationProvider.Facebook); request.Headers['X-ZUMO-AUTH'] = App.MobileService.CurrentUser.MobileServiceAuthenticationToken; } catch (Exception ex) {} response = await base.SendAsync(request, cancellationToken); } } }

ServiceFilter (iOS)- (void) filterResponse: (NSHTTPURLResponse *) response forData: (NSData *) data withError: (NSError *) error forRequest:(NSURLRequest *) request onNext:(MSFilterNextBlock) onNext onResponse: (MSFilterResponseBlock) onResponse{ if (response.statusCode == 401) { [self.client loginWithProvider:@"facebook" onController:[[[[UIApplication sharedApplication] delegate] window] rootViewController]

animated:YES completion:^(MSUser *user, NSError *error) { if (error && error.code == -9001) { [self busy:NO]; onResponse(response, data, error); return; } NSMutableURLRequest *newRequest = [request mutableCopy]; [newRequest setValue:self.client.currentUser.mobileServiceAuthenticationToken forHTTPHeaderField:@"X-ZUMO-AUTH"]; onNext(newRequest, ^(NSHTTPURLResponse *innerResponse, NSData *innerData, NSError *innerError){ [self filterResponse:innerResponse forData:innerData withError:innerError forRequest:request onNext:onNext onResponse:onResponse]; }); }]; } else { [self busy:NO]; onResponse(response, data, error); }}

Auth Demo

One-to-Many

ClientServer

Clientpublic class BigTodo { public int Id { get; set;} ... [IgnoreDataMember] public List<LittleTodo> LittleTodos { get; set; }

}

//writing dataprivate async Task InsertBigTodo(MobileServiceClient mobileServiceClient, BigTodo bigTodo){ var bigTodoTable = mobileServiceClient.GetTable<BigTodo>(); await bigTodoTable.InsertAsync(bigTodo); var bigTodoId = bigTodo.Id; var littleTodosTable = mobileServiceClient.GetTable<LittleTodo>(); foreach (var littlTodo in bigTodo.LittleTodos) { littleTodo.BigTodoId = bigTodoId; await littleTodosTable.InsertAsync(littleTodo); }}

Server 1function insert(item, user, request) { var littleTodosTable = tables.getTable('LittleTodo'); var littleTodos = item.LittleTodos; var ids = new Array(littleTodos.length); var count = 0; littleTodos.forEach(function(littleTodo, index) { littleTodosTable.insert(littleTodos, { success: function() { // keep a count of callbacks count++; // build a list of new ids - make sure // they go back in the right order ids[index] = littleTodos.id; if (littleTodos.length === count) { // we've finished all updates, // send response with new IDs request.respond(201, { littleTodoIds: ids }); } } }); });}

Server 2function insert(item, user, request) { var littleTodos; if (item.LittleTodos) { littleTodos = item.LittleTodos; delete item.LittleTodos; } request.execute({ success: function () { item.LittleTodos = []; if (littleTodos) { var i = 0; var insertNext = function () { if (i < littleTodos.length) { var littleTodo = littleTodos[i]; littleTodo.BigTodoId = item.id; littleTodo.LittleTodoOrder = i; tables.getTable('LittleTodo').insert(littleTodo, { success: function () { item.LittleTodos.push(littleTodo); i++; insertNext(); } }); } else { request.respond(); } }; insertNext(); } } });}

Remember API call #s when considering client side one-to-many

Paging Data

ClientServer

ClientC#:IMobileServiceTableQuery<TodoItem> query = todoTable .Where(todoItem => todoItem.Complete == false) .Skip(3) .Take(3);items = await query.ToCollectionAsync();ListItems.ItemsSource = items;

iOS:NSPredicate * predicate = [NSPredicate predicateWithFormat:@"complete == NO"];MSQuery * query = [self.table queryWithPredicate:predicate];query.includeTotalCount = YES; // Request the total item count

query.fetchOffset = 3;query.fetchLimit = 3;

[query readWithCompletion:^(NSArray *results, NSInteger totalCount, NSError *error) { …

Android:mToDoTable.where().field("complete").eq(false).skip(3).top(3) .execute(new TableQueryCallback<ToDoItem>() {

Server ScriptsOption 1:query.where({complete: false}) .take(3) .skip(3);

Option 2:var q = query.getComponents();q.take = 3;q.skip = 1;query.setComponents(q);

Option 3:query.where(function() {

return this.complete == false}) .take(3) .skip(3);

On-Prem

On-Prem Solutions in Windows Azure

Secure Site-to-Site Network Connectivity

Windows Azure Virtual Network

CLOUD ENTERPRISE

Data Synchronization

SQL Data Sync

Application-Layer Connectivity &

Messaging Service Bus

Secure Machine-to-Machine Network

ConnectivityWindows Azure Connect

Secure Point-to-Site Network Connectivity

Windows Azure Virtual Network

Service Bus RelaysCorporate NetworkWindows Azure

Database

Service Bus ApplicationSQLMobile Service Cloud Worker

Corporate NetworkWindows Azure

Mobile Service On-premise AppService Bus

On-premises

Your datacenter

Individual computers behind corporate firewall

Point-to-Site VPN

Route-based VPN

Windows Azure

Virtual NetworkVPN Gateway

<subnet 1>

<subnet 2>

<subnet 3> DNS

Server

VPN Gateway

Remote devices

Site-to-SiteVPN

Point-to-Site VPNs

On-premises

Your datacenter

Hardware VPN or Windows RRAS

Windows Azure

Virtual NetworkVPN Gateway

<subnet 1>

<subnet 2>

<subnet 3> DNS

Server

VPN Gateway

Site-to-SiteVPN

Site-to-Site Connectivity• Extend your premises to the cloud securely• On-ramp for migrating services to the cloud• Use your on-prem resources in Azure

(monitoring, AD, …)

• Hybrid Networking with Windows Azure – http://aka.ms/zumoprem1

• Windows Azure Websites and On-Prem http://aka.ms/zumoprem2

Links for more

ResourcesGet a Windows Azure Free Trial Accounthttp://www.windowsazure.com

Videos, Tutorials and morehttp://www.windowsazure.com/mobile

Mobile Services Resourceshttp://aka.ms/CommonWAMS

Contact mehttp://chrisrisner.com@chrisrisner

Mobile Services at BuildMobile Services – Soup to NutsJosh Twist – Thursday 2pm

Protips for Mobile ServicesChris Risner – Thursday 5pm

Cross-Platform w/ Mobile ServicesChris Risner – Thursday 4pm

Connected Win Phone AppsYavor Georgiev – Friday 9am

Going Live and BeyondKirill and Paul – Friday 10:30am

Delivering Push Notifications to MillionsElio Demaggio – Friday 12pm

Who’s that user?Dinesh Kulkarni – Friday 2pm

Developing Windows 8.1 apps using Windows Azure Mobile Services Nick Harris – Friday 2pm

© 2013 Microsoft Corporation. All rights reserved. Microsoft, Windows, Windows Vista and other product names are or may be registered trademarks and/or trademarks in the U.S. and/or other countries.The information herein is for informational purposes only and represents the current view of Microsoft Corporation as of the date of this presentation. Because Microsoft must respond to changing market conditions, it should not be interpreted to be a commitment on the part of Microsoft, and Microsoft cannot guarantee the accuracy of any information provided after the date of this presentation. MICROSOFT MAKES NO WARRANTIES, EXPRESS, IMPLIED OR STATUTORY, AS TO THE INFORMATION IN THIS PRESENTATION.

Recommended