After thinking about making clients to access the game and the fact that it is probably easier to create a generator tool on a desktop client (Thinking Windows 8 here); I thought it would probably be best to create an Web API web role now rather than later. The website after all is basically a Client as well.
So new way of thinking. All clients including the website don’t know anything about Entity Framework or Azure Service bus. All they need to know about is how to access the API. Having this as an extra Tier will also allow easier scaling in future.
Looks like I’ll be hitting the Pluralsight ASP.NET Web API course this weekend.
2012/11/28
Adding in an Extra Tier
2012/11/22
I feel a project coming together
As a way of trying to put all the stuff that I am learning together. I’ve come up with a project. I have been attempting to write a game for ages and have decided to make it using all the new Microsoft technologies. (You may have seen some of the preliminary work resulting in blog posts here.)
So this project will be a web based RPG game (With mobile clients to come later hopefully). It will be hosted in Azure and it will make use of Web Roles, Worker roles, SQL Azure, Azure Service Bus/ACS and Azure Storage. It will be written in Visual Basic .Net.
This is the website: https://www.tyranntrpg.org/
The front page has more details on what is going on and I hope to update the progress regularly here on this blog.
The current site doesn’t do too much as most of the work is going on behind the scenes. But the Codex part will get updates as things progress. In the images section of the codex you can see some of the artwork done by http://www.battleaxegfx.com/
This is one of the images (It will eventually be the Something has gone wrong image)
This project has been going on for a few years using different tools to create hence the collection of artwork and ideas.
Hopefully this time I’ll get it finished 🙂
2012/11/19
Hit a 404 error with a dot in an MVC 4 URL
Another quick post regarding passing a parameter that contains a “.” in it.
For example the below URL
http://myhost/home/edituser/me@mail.com
Started to cause a 404 error. I wasn’t sure why this was happening and after a lot of web surfing I came across the following information on Stack Overflow
Adding the following line into the HTTP <handlers> section for the path that contains the error:
<system.webServer> .... <handlers> .... <add name="ApiURIs-ISAPI-Integrated-4.0" path="/home/edituser/*" verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" /> </handlers> </system.webServer>
Will fix the issue.
Displaying Formatted XML in an ASP.net Razor Web Page
A quick post for those looking to display a formatted XML document on an ASP.net Razor web page. The following code is in the controller and used to create the string to get passed through to the view. This example is based on displaying the information pulled from the Azure Management API as mentioned in previous blogs. The data is stored in a SQL Server database.
Function ShowRawXML(serviceOperationId As String, subscriptionName As String) As ActionResult Dim model As New ShowRawXMLViewModel Dim op = (From o In _db.ServiceOperations Where o.ServiceOperationId = serviceOperationId And o.Subscription.FriendlyName = subscriptionName Select o).Single model.RawXML = vbCr & Server.HtmlEncode(op.RawXML) model.SubscriptionName = subscriptionName model.SubscriptionID = op.Subscription.SubscriptionID model.ServiceOperationID = serviceOperationId model.ServiceOperationType = op.Type.Name Return View(model) End Function
In the view, we are going to use the @Html.Raw function so we used the Server.HtmlEncode function to make it safe to display in HTML. Next we get to the razor view…
@ModelType AzureManager.Domain.ShowRawXMLViewModel @Code ViewData("Title") = "ShowRawXML" End Code <h2>Subscription ID [@Model.SubscriptionID] @Model.SubscriptionName</h2> <h3>Show Raw XML for Service Operation @Model.ServiceOperationID</h3> <h3>Operation Type: @Model.ServiceOperationType</h3> <pre> @Html.Raw(Model.RawXML) </pre> <p> @Html.ActionLink("Back to List", "ServiceOperations", New With {.Id = Model.SubscriptionName}) </p>
Here you notice we display the raw XML inside a <pre></pre> tag. This will preserve the white space in the string. More information on this here: HTML <pre> Tag
2012/11/13
A VB example to access the azure management REST API
Hi,
This is based on an article in the Code Quick Start from MSDN, Original article is here: Code Quick Start: Create a console application that lists your Windows Azure hosted services
Below is the code which has been converted to VB.net. Everything else in the article is the same apart from creating a VB console program.
Imports System.Net Imports System.Security.Cryptography.X509Certificates Imports System.IO Module Module1 Sub Main() Try ' X.509 certificate variables Dim certStore As X509Store Dim certCollection As X509Certificate2Collection Dim certificate As X509Certificate2 ' Request and response variables Dim httpRequest As HttpWebRequest Dim httpResponse As HttpWebResponse ' Stream variables Dim responseStream As Stream Dim reader As StreamReader ' URI variable Dim requestURI As Uri ' Specify operation to use for the service management call. ' This sample will use the operation for listing the hosted services. Dim operation As String = "hostedservices" ' The ID for the Windows Azure subscription. Dim subscriptionId As String = "{Your Subscription ID}" ' The thumbprint for the certificate. This certificate would have been ' previously added as a management certificate within the Windows ' Azure management portal. Dim thumbPrint As String = "{Your Certificate Thumbprint}" ' Open the certificate store for the current user. certStore = New X509Store(StoreName.My, StoreLocation.CurrentUser) certStore.Open(OpenFlags.ReadOnly) ' Find the certificate with the specified thumbprint certCollection = certStore.Certificates.Find( X509FindType.FindByThumbprint, thumbPrint, False) ' close the certificate store certStore.Close() ' Check to see if mat If certCollection.Count = 0 Then Throw New Exception("No certificate found containing thumbprint " _ & thumbPrint) End If ' A matching certificate was found. certificate = certCollection(0) Console.WriteLine("Using certificate with thumbprint: " & thumbPrint) ' create new request requestURI = New Uri( String.Format("https://management.core.windows.net/{0}/services/{1}", subscriptionId, operation)) httpRequest = HttpWebRequest.Create(requestURI) ' add certificate to requrest httpRequest.ClientCertificates.Add(certificate) ' Specify the version information in the header httpRequest.Headers.Add("x-ms-version", "2011-10-01") ' Make the call using the web request httpResponse = httpRequest.GetResponse ' Display the response status code Console.WriteLine("Response status code: " _ & httpResponse.StatusCode) ' Display thr request ID returned by windows azure If httpResponse.Headers IsNot Nothing Then Console.WriteLine("x-ms-request-id: " _ & httpResponse.Headers("x-ms-request-id")) End If ' Parse the web response responseStream = httpResponse.GetResponseStream reader = New StreamReader(responseStream) ' Displa the raw response Console.WriteLine("Response output:") Console.WriteLine(reader.ReadToEnd) ' close the resources no longer needed httpResponse.Close() responseStream.Close() reader.Close() Console.ReadKey() Catch ex As Exception Console.WriteLine("Error encountered: " & ex.Message) Console.ReadKey() System.Environment.Exit(1) Finally System.Environment.Exit(0) End Try End Sub End Module
2012/11/08
OData AddTo using WinRT and VB.net
I was trying to find a way to POST a new entity to an OData feed and found it hard to find examples of doing this. After a bit of searching I found some C# code that allowed me to do an update. After converting this to VB I ended up with
Private Sub addPersonButton_Click_1(sender As Object, e As RoutedEventArgs) Dim p As New Person p.name = personNameTextBox.Text If p.name <> "" Then _context.AddToPerson(p) _context.BeginSaveChanges(AddressOf ContextSaveChanges, p) End If End Sub
Person is part of an OData feed I am using e.g. http://server/odata.svc/Person
So I create a new instance of a Person and add a name to it (From a TextBox in the UI)
Then I use the AddToPerson method of the context and as there is no synchronous way in WinRT to SaveChanges, I used the BeginSaveChanges method of the context and passed through the address of the routine that will handle the callback.
Private Function ContextSaveChanges(result As IAsyncResult) As Task Try _context.EndSaveChanges(result) Catch ex As Exception _message = ex.Message If ex.InnerException IsNot Nothing Then _message = _message & " - " & ex.InnerException.Message End If Me.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, AddressOf UpdateUI) End Try End Function Public Sub UpdateUI() personNameTextBox.Text = _message End
Above is the function that gets called and it will try to run EndSaveChanges and trap any error that occurred and display it on the UI. I originally put the personNameTextBox.Text = ex.Message in the catch section but that caused a Treading error [RPC_E_WRONG_THREAD]. So after a lot of routing around and the help of a colleague I discovered the Me.Dispacher.RunAsync function in which I had a lambda expression in to update the UI. This didn’t fail but also didn’t update the UI so I used a subroutine and put the AddressOf in the RunAsync command and this worked as expected.
Phew made it 😉
2012/11/07
Windows Azure, Service Bus Queue between Webrole and Worker role
In a previous post I showed Azure Storage Queues being used to send messages between a Webrole and a Worker role.
I’ve got back onto this project to learn more about MCV 4 and Windows Azure Service Bus Queues. So I modified the original classes to use the SB Queues instead of Azure Storage queues. Behind the scenes Entity Framework Code First is being used to access the database. I’m loving the ability to change the classes and then run update-database to modify the SQL Azure database. Back to the queues.
First you need to create a queue which this article can walk you you through.
Once the queue is created put the connection string into the Azure Service Configuration file.
Below is the base class for a Job for this application.
Imports System Imports System.Collections.Generic Imports System.Diagnostics Imports System.Linq Imports System.Net Imports System.Threading Imports Microsoft.ServiceBus Imports Microsoft.ServiceBus.Messaging Imports Microsoft.WindowsAzure Imports Microsoft.WindowsAzure.Diagnostics Imports Microsoft.WindowsAzure.ServiceRuntime Imports Microsoft.WindowsAzure.Storage Public MustInherit Class tjob ' Enable database access Protected Friend _db As New TyranntDb() ' A place holder for the user information Protected Friend _usr As Member ' setup variables to allow for access to Azure Queues Protected Friend _jobQueue As QueueClient ' A sample of a job entry in the Queue Private sample = <job type="email" userid="102"> <type/> </job> ' the user ID which is used to pull the user information Private _userID As Int64 ' Initialise the job from the XML String Public Sub New(jobStr As String) Dim jobXML As XElement = XElement.Parse(jobStr) _JobType = jobXML.@type Dim usrstr As String = jobXML.@userid Try UserID = Convert.ToInt64(usrstr) Catch ex As Exception ErrorMessage = ex.Message ReportError("tJob New Convert int64") End Try End Sub ' Create a blank job, this is used for creating a job to ' put onto the queue. Public Sub New() _JobType = "" _userID = -1 End Sub ' Job type. Used to create the correct object. Public Property JobType As String ' The user ID. If this is being set then it ' will look up the user from the database Public Property UserID As Integer Get Return _userID End Get Set(value As Integer) _userID = value If _userID > 0 Then GetUserDetails() End If End Set End Property ' This is the code that "Processes" the job. Each job type must ' implement this code. Public MustOverride Function Process() As Boolean ' A general variable for storing any errors that ' occur. If it's empty then no errors are assumed. Public Property ErrorMessage As String ' This will generate an XML element that describes the job. Public MustOverride Function ToXML() As XElement ' This will generate a string version of the XML ' which describes this job. Public Overrides Function ToString() As String Return ToXML.ToString End Function ' This routine will pull the user information from the ' database and store the user detals in the _usr object. Protected Friend Sub GetUserDetails() Dim q = From u In _db.Members Where u.ID = _userID Select u If q.Count > 0 Then _usr = q.Single End If End Sub ' If the job is being created. This function will add the job ' to the Azure Queue. Public Sub AddJobToQueue() ' Get the azure storage account object. _jobQueue = QueueClient.CreateFromConnectionString(CloudConfigurationManager.GetSetting("Microsoft.ServiceBus.ConnectionString"), "standard_queue") Try ' Now add the job details to the queue. Dim msg As New BrokeredMessage(Me.ToString) _jobQueue.Send(msg) Catch ex As Exception _ErrorMessage = ex.Message ReportError("AddJobToQueue") End Try End Sub Public Sub ReportError(location As String) Dim err As New ErrorMessage err.ErrorTime = DateTime.Now err.Location = location err.Message = Me.ErrorMessage _db.ErrorMessages.Add(err) _db.SaveChanges() End Sub End Class
As you can see, this class must be inherited by another class which will perform the actual job. The base class will handle getting a users information from the database represented by the TyranntDB context class. I’ve added an error reporting system which will store any errors into a table in the database. The job information is stored in an XML format (The message queues allow for 64kb message which should be plenty for our purposes), so the inheriting class must implement a ToXml method. Also the inheriting job class must know how to do the job it is intended to perform so it must implement a Process method as well. Finally the class must be able to add its self to the message queue. So this base class has an AddJobToQueue method which makes use of the ToXml method the inheriting class has implemented to generate the message body to be added onto the queue. The service bus Imports at the top of the class will expose the QueueClient and the BrokeredMessage which is all that is required to add a message to the queue. You can see in the code how simple it is.
Next we need to add a new class that is derived from this base class to add a new member to the database. Below is the code that achieves this.
Public Class tjNewUser Inherits tjob ' an example of a new user job Private sample = <job type="newuser" userid="-1"> <user name="{username}" email="{user)@{domain}">Full Name</user> </job> ' Extra data required by this class Public Property userName As String Public Property email As String Public Property fullName As String Public Sub New(jobStr As String) ' initialise basic information MyBase.New(jobStr) Dim jobXML As XElement = XElement.Parse(jobStr) ' now initialise new user information userName = jobXML...<user>.@name email = jobXML...<user>.@email fullName = jobXML...<user>.Value End Sub Public Sub New() ' initialise the base information MyBase.New() JobType = "newuser" userName = "" email = "" fullName = "" End Sub ' Create the new user in the database Public Overrides Function Process() As Boolean ' first check to see if the user already exists Try Dim r = From m In _db.Members Where m.MemberAlias = _userName Select m If r.Count > 0 Then ' User already exists so do not continue ' return true in this case as request ' has been processed more than one. ErrorMessage = "User " & _userName & " Already exists" ReportError("tjNewUser Process") Return True End If Catch ex As Exception ErrorMessage = ex.Message ReportError("tjNewUser lynq query to get member ID") End Try ' create a new user Dim usr As New Member ' populate the generic information usr.EmailAddress = _email usr.Name = _fullName usr.MemberAlias = _userName usr.LastLogin = DateTime.Now ' now set the user group to be member Dim userType As MemberType Try userType = (From m In _db.MemberTypes Where m.Name = "Member" Select m).Single Catch ex As Exception ErrorMessage = ex.Message ReportError("tjNewUser Lynq query to get 'Member' member type") Return False End Try usr.Type = userType ' now save the user Try _db.Members.Add(usr) _db.SaveChanges() Catch ex As Exception ErrorMessage = ex.Message ReportError("tjNewUser Memebers.Add Save Changes") Return False End Try ' Now that the user was sucessfully created, ' generate a new user email job Dim jb As New tjEmail jb.EmailType = "NewAccount" jb.UserID = usr.ID ' Add the job to the Azure job queue jb.AddJobToQueue() If jb.ErrorMessage = "" Then Return True Else ErrorMessage = jb.ErrorMessage ReportError("tjNewUser Add Job to Queue produced error") Return False End If End Function Public Overrides Function ToXML() As XElement Return <job type="newuser" userid=<%= UserID %>> <user name=<%= _userName %> email=<%= _email %>><%= _fullName %></user> </job> End Function End Class
As you can see, this class has overrode the ToXML function and the Process Function which has the code that actually adds the user to the Members database. In the last part of the process function it creates a new job which will be used to send an email to the user about their newly created account. This job class is shown below.
Imports System.Net.Mail Imports Microsoft.WindowsAzure Public Class tjEmail Inherits tjob ' a sample email job Private sample = <job type="email" userid="102"> <email from="noreply@tyranntrpg.org" type="newuser"/> </job> ' setup extra information required by this job Private _from As String Private _emailType As String ' The is the from email address Public WriteOnly Property From As String Set(value As String) _from = value End Set End Property ' This will be the email type e.g. newuser Public WriteOnly Property EmailType As String Set(value As String) _emailType = value End Set End Property ' If the job XML already exists this will set up ' the information automatically Public Sub New(jobStr As String) MyBase.new(jobStr) Dim jobXML As XElement = XElement.Parse(jobStr) _from = jobXML...<email>.@from _emailType = jobXML...<email>.@type End Sub ' Create an empty email job if creating a new job Public Sub New() MyBase.New() JobType = "email" _from = "noreply@tyranntrpg.org" _emailType = "" End Sub '' Send the email Public Overrides Function Process() As Boolean Dim email As MailMessage ' Generate the correct body of the email Select Case _emailType Case "NewAccount" email = GenerateNewUserEmail() Case Else ErrorMessage = String.Format("Email Type [{0}] not recognised", _emailType) ReportError("tjEmail Process") Return False End Select Dim smtp = New SmtpClient(CloudConfigurationManager.GetSetting("SMTPAddress"), Integer.Parse(CloudConfigurationManager.GetSetting("SMTPPort"))) smtp.Credentials = New Net.NetworkCredential(CloudConfigurationManager.GetSetting("SMTPUser"), CloudConfigurationManager.GetSetting("SMTPPassword")) smtp.EnableSsl = True Try smtp.Send(email) Catch ex As Exception Me.ErrorMessage = ex.Message ReportError("tjEmail Send()") Return False End Try Return True End Function ' This will generate the subject and body of the newuser email Private Function GenerateNewUserEmail() As MailMessage If _usr Is Nothing Then ErrorMessage = "_usr is null" ReportError("GenerateNewUserEmail()") Return Nothing End If Dim email As New MailMessage(_from, _usr.EmailAddress) Dim subject As String = "" Dim body As String = "" Try Dim emailMsg = (From e In _db.EmailMessages Where e.Name = "NewAccount" Select e).Single subject = emailMsg.Subject body = String.Format(emailMsg.Body, _usr.Name) Catch ex As Exception ErrorMessage = ex.Message ReportError("GenerateNewUserEmail(), Lynq Query to _db.EmailMessages") Return Nothing End Try ErrorMessage = body email.Subject = subject email.Body = body Return email End Function Public Overrides Function ToXML() As XElement Return <job type="email" userid=<%= UserID %>> <email from=<%= _from %> type=<%= _emailType %>/> </job> End Function Private Sub Smtp_SendCompleted(sender As Object, e As ComponentModel.AsyncCompletedEventArgs) If e.Error Is Nothing Then ErrorMessage = "Mail sent correctly" Me.ReportError("tjEmail SendCompleted") Else ErrorMessage = e.Error.Message Me.ReportError("tjEmail SendCompleted") End If End Sub End Class
All the SMTP server information is held in the azure service configuration file. Again the overrode functions do all the work in this class.
Now onto the back end. This is uses the Worker Role With Service Bus Queue template (Shown Below)
As the job classes actually do all the work required, this is a very small piece of code.
Imports System Imports System.Collections.Generic Imports System.Diagnostics Imports System.Linq Imports System.Net Imports System.Threading Imports Microsoft.ServiceBus Imports Microsoft.ServiceBus.Messaging Imports Microsoft.WindowsAzure Imports Microsoft.WindowsAzure.Diagnostics Imports Microsoft.WindowsAzure.ServiceRuntime Imports Microsoft.WindowsAzure.Storage Imports Tyrannt.Domain Public Class WorkerRole Inherits RoleEntryPoint ' The name of your queue Const QueueName As String = "standard_queue" Private _db As New TyranntDb ' QueueClient is Thread-safe. Recommended that you cache ' rather than recreating it on every request Dim Client As QueueClient Dim IsStopped As Boolean Public Overrides Sub Run() ' This is a sample implementation for Tyrannt.Backoffice. Replace with your logic. While (Not IsStopped) Try ' Receive the message Dim receivedMessage As BrokeredMessage = Client.Receive() If (Not receivedMessage Is Nothing) Then ' Process the message Trace.WriteLine("Procesing", receivedMessage.SequenceNumber.ToString()) ProcessMessage(receivedMessage) receivedMessage.Complete() End If Catch ex As MessagingException If (Not ex.IsTransient) Then Trace.WriteLine(ex.Message) Throw ex End If Thread.Sleep(10000) Catch ex As OperationCanceledException If (Not IsStopped) Then Trace.WriteLine(ex.Message) Throw ex End If End Try End While End Sub Private Sub ProcessMessage(msg As BrokeredMessage) Dim msgBody As String = "msgBody not available" Dim errMsg As String = "" Try msgBody = msg.GetBody(Of String)() ' Turn the message into an XML element Dim xmlMsg As XElement = XElement.Parse(msgBody) ' Extract the message type from the element Dim type As String = xmlMsg.@type ' Now we create a job Dim job As tjob 'ReportError("Processing job [" & type & "]", "WorkerRole.vb") Select Case type ' Use the message type to see what kind of job is required Case "newuser" job = New tjNewUser(xmlMsg.ToString) Case "email" job = New tjEmail(xmlMsg.ToString) Case Else ReportError("job type [" + type + "] Not recognised", "WorkerRole.ProcessMessage()") Exit Sub End Select ' Process the job. If job.Process() = True Then ' The job succeeded so write a trace message to say this and ' delete the message from the queue. Trace.WriteLine(String.Format("{0} succeeded", type), "Information") Else ' The job failed so write a trace error message saying why the job failed. ' This will leave the job on the queue to be processed again. Trace.WriteLine(String.Format("{0} failed: {1} ", type, job.ErrorMessage), "Error") End If Catch ex As Exception ' something big has gone wrong so write this out as an error trace message. Trace.WriteLine(String.Format("Failed to parse xml message: [{0}]", msgBody, "Error")) ReportError(ex.Message, "WorkerRole.ProcessMessage()") Exit Sub End Try End Sub Public Overrides Function OnStart() As Boolean ' Set the maximum number of concurrent connections ServicePointManager.DefaultConnectionLimit = 12 ' Create the queue if it does not exist already Dim connectionString As String = CloudConfigurationManager.GetSetting("Microsoft.ServiceBus.ConnectionString") Dim namespaceManager As NamespaceManager = namespaceManager.CreateFromConnectionString(connectionString) If (Not namespaceManager.QueueExists(QueueName)) Then namespaceManager.CreateQueue(QueueName) End If ' Get a client to use the queue Client = QueueClient.CreateFromConnectionString(connectionString, QueueName) IsStopped = False Return MyBase.OnStart() End Function Public Overrides Sub OnStop() ' Close the connection to Service Bus Queue IsStopped = True Client.Close() MyBase.OnStop() End Sub Private Sub ReportError(msg As String, location As String) Dim err As New ErrorMessage err.ErrorTime = DateTime.Now err.Location = location err.Message = msg _db.ErrorMessages.Add(err) _db.SaveChanges() End Sub End Class
I’ve added the ability to access the database to this class only for error reporting which helps tracking down any issues during development. The only subroutine aside from the error routines is the ProcessMessage. This function will get the message body. Work out what type of message it is and then generate the correct class and run the Process method. (Note: You can only use the GetBody method once, if you try more than once it will crash out.) In this example I complete the received message even if it fails to process (Failure gets added to the error table). In future I may end up doing more elaborate things.
I hope some people find this helpful.
2012/10/30
Accessing and OData Feed in WinRT App using Visual Basic.NET
As I’ve been searching the internet to try and find ways of accessing OData Feeds in visual basic .net and not finding much help I thought I’d put together a small example to help others.
This will use a demo OData feed to pull customer records from an example banking application. The feed is located in an Azure Cloud Service and is accessed by the following URL
http://t24irisdemo.cloudapp.net/tbank/Wealth.svc/
(This feed may be removed in the future)
The first thing to do is create a Visual Basic Blank Windows Store XAML application.
Also add a basic page called CustomerPage.
Add a Service Reference which points to the above URL
We will only use Customers from the feed.
First display all the customers. To do this we need to add a Gridview to the main page XAML with an Item Template to display customer information. (I’m not a designer so I apologies for the rough look of the details 😉 )
<GridView x:Name="defaultGridView" Grid.Row="1" Tapped="defaultGridView_Tapped_1" Margin="10" BorderBrush="#FFF7EC05" BorderThickness="1" FontFamily="Global User Interface" > <GridView.ItemTemplate> <DataTemplate> <Grid Width="250" Height="135" Background="#002"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="100" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <TextBlock Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="0" Text="{Binding Path=CustomerCode}" HorizontalAlignment="Center" FontWeight="ExtraBold" ></TextBlock> <Image Grid.Column="0" Grid.Row="1" Grid.RowSpan="2" Source="{Binding Path=Photo}" Stretch="Uniform" Margin="4" VerticalAlignment="Center" HorizontalAlignment="Center"/> <TextBlock Grid.Column="1" Grid.Row="1" FontStyle="Italic" VerticalAlignment="Bottom">Name:</TextBlock> <TextBlock Grid.Column="1" Grid.Row="2" Text="{Binding Path=Name}" VerticalAlignment="Top" HorizontalAlignment="Left" TextWrapping="Wrap"></TextBlock> </Grid> </DataTemplate> </GridView.ItemTemplate> </GridView>
Next add the following code to the code behind for the Main Page.
Imports System.Data.Services.Client ' The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=234238 ''' <summary> ''' An empty page that can be used on its own or navigated to within a Frame. ''' </summary> Public NotInheritable Class MainPage Inherits Page Private Const IRISURL As String = "http://t24irisdemo.cloudapp.net/tbank/Wealth.svc" Private _context As New irisServiceReference.T24Wealth(New Uri(IRISURL)) Private WithEvents _allCust As DataServiceCollection(Of irisServiceReference.Customers) ''' <summary> ''' Invoked when this page is about to be displayed in a Frame. ''' </summary> ''' <param name="e">Event data that describes how this page was reached. The Parameter ''' property is typically used to configure the page.</param> Protected Overrides Sub OnNavigatedTo(e As Navigation.NavigationEventArgs) _allCust = New DataServiceCollection(Of irisServiceReference.Customers) AddHandler _allCust.LoadCompleted, AddressOf LoadAllCustComplete Dim query = _context.Customers _allCust.LoadAsync(query) End Sub Private Sub LoadAllCustComplete(sender As Object, e As LoadCompletedEventArgs) defaultGridView.ItemsSource = _allCust End Sub Private Sub defaultGridView_Tapped_1(sender As Object, e As TappedRoutedEventArgs) Dim gv As GridView = sender Dim item As irisServiceReference.Customers = gv.SelectedItem Dim customerNo As String = customerNo = item.CustomerCode Frame.Navigate(GetType(CustomerPage), customerNo) End Sub End Class
The above code will pull all the customers from the feed and then bind them to the GridView. There is also a Tap event handler which will pull the customer number from the grid view item and navigate to the customer view page (I’ll leave this to you to have a go a designing the look and feel. In the following code example I assumed there is a text block called customerNameTextBlock).
If you put a debug point at the _allCust.LoadAsync(query) line. You will notice that Query is turned into a URL to the feed.
query = {http://t24irisdemo.cloudapp.net/tbank/Wealth.svc/Customers}
Next we need to put code behind the Customer info page to pull the tapped customer details back so that we can populate this page. The following code with do this.
' The Basic Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=234237 Imports System.Data.Services.Client ''' <summary> ''' A basic page that provides characteristics common to most applications. ''' </summary> Public NotInheritable Class CustomerPage Inherits Common.LayoutAwarePage Private Const IRISURL As String = "http://t24irisdemo.cloudapp.net/tbank/Wealth.svc" Private _context As New irisServiceReference.T24Wealth(New Uri(IRISURL)) Private WithEvents _allCust As DataServiceCollection(Of irisServiceReference.Customers) ''' <summary> ''' Populates the page with content passed during navigation. Any saved state is also ''' provided when recreating a page from a prior session. ''' </summary> ''' <param name="navigationParameter">The parameter value passed to ''' <see cref="Frame.Navigate"/> when this page was initially requested. ''' </param> ''' <param name="pageState">A dictionary of state preserved by this page during an earlier ''' session. This will be null the first time a page is visited.</param> Protected Overrides Sub LoadState(navigationParameter As Object, pageState As Dictionary(Of String, Object)) End Sub ''' <summary> ''' Preserves state associated with this page in case the application is suspended or the ''' page is discarded from the navigation cache. Values must conform to the serialization ''' requirements of <see cref="Common.SuspensionManager.SessionState"/>. ''' </summary> ''' <param name="pageState">An empty dictionary to be populated with serializable state.</param> Protected Overrides Sub SaveState(pageState As Dictionary(Of String, Object)) End Sub Protected Overrides Sub OnNavigatedTo(e As Navigation.NavigationEventArgs) Dim customerNo As String = e.Parameter pageTitle.Text = "Customer " + customerNo _allCust = New DataServiceCollection(Of irisServiceReference.Customers) AddHandler _allCust.LoadCompleted, AddressOf allCustomerLoadCompleted Dim query = From c In _context.Customers Where c.CustomerCode = customerNo Select c _allCust.LoadAsync(query) End Sub Private Sub allCustomerLoadCompleted(sender As Object, e As LoadCompletedEventArgs) customerNameTextBlock.Text = _allCust.Single.Name End Sub End Class
You’ll notice in this piece of code that a Linq query is used to pull the customer record from the feed. If you again put a debug point at _allCust.LoadAsync(query) you will notice the query is also transformed into a OData URL.
query = {http://t24irisdemo.cloudapp.net/tbank/Wealth.svc/Customers()?$filter=CustomerCode eq '100320'}
Hopefully this very simple example will help you get kick-started in developing WinRT applications that access OData feed.
2012/09/21
Sort of found the answer to my sharing issue with WinRT & Dropbox
It turns out, if you have the same folder location on two different machines. E.g. the default drop box location c:\users\{user}\dropbox, Â this makes the project fail to run on a the machine that wasn’t the one it was created on.
If you move the dropbox folder to a new location on the other machine, E.g. c:\Dropbox then it will run with no issues. A weird one this, no one had an answer to why it happens.
Hopefully if someone else has this issue, then this post may help them.
BTW: The person at the Workshop seemed to be taken aback that the app was written using VB too 😉