|
|
9 éve | |
|---|---|---|
| .. | ||
| Finish | 9 éve | |
| Start | 9 éve | |
| README.md | 9 éve | |
| Speaker.csv | 9 éve | |
Today, we will be building a cloud connected Xamarin.Forms application that will display a list of speakers at Xamarin Dev Days and show their details. We will start by building some business logic backend that pulls down json from a RESTful endpoint and then we will connect it to an Azure Mobile App backend in just a few lines of code.
Open Start/DevDaysSpeakers.sln
This solution contains 4 projects
The DevDaysSpeakers (Portable) also has blank code files and XAML pages that we will use during the Hands on Lab.
All projects have the required NuGet packages already installed, so there will be no need to install additional packages during the Hands on Lab. The first thing that we must do is restore all of the NuGet packages from the internet.
This can be done by Right-clicking on the Solution and clicking on Restore NuGet packages...
We will be pulling down information about spearks. Open the DevDaysSpeakers/Model/Speaker.cs file and add the following properties inside of the Speaker class:
public string Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Website { get; set; }
public string Title { get; set; }
public string Avatar { get; set; }
The SpeakersViewModel.cs will provide all of the functionality for how our main Xamarin.Forms view will display data. It will consist of a list of speakers and a method that can be called to get the speakers from the server. It will also contain a boolean flag that will indicate if the we are getting data in a background task.
INotifyPropertyChanged is important for data binding in MVVM Frameworks. This is an interface that, when implemented, let's our view know about changes to the model.
Update:
public class SpeakersViewModel
{
}
to
public class SpeakersViewModel : INotifyPropertyChanged
{
}
Simply right click and tap Implement Interface, which will add the following line of code:
public event PropertyChangedEventHandler PropertyChanged;
This is the method that we will invoke whenever a property changes. This means we need a helper method to implement called OnPropertyChanged:
void OnPropertyChanged([CallerMemberName] string name = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
void OnPropertyChanged([CallerMemberName] string name = null)
{
var changed = PropertyChanged;
if (changed == null)
return;
changed.Invoke(this, new PropertyChangedEventArgs(name));
}
Now, we can call OnPropertyChanged(); whenever a property updates. Let's create our first bindable property now.
In the class, we will want to create a backing field and auto-properties for getting and setting a boolean. This will be useful for letting our view know that our view model is busy, and to make sure we aren't performing duplicate operations at the same time (like allowing the user to refresh the data multiple times).
First, create the backing field:
bool busy;
Next, let's create the auto-property:
public bool IsBusy
{
get { return busy; }
set
{
busy = value;
OnPropertyChanged();
}
}
Notice that we call OnPropertyChanged(); so Xamarin.Forms can be notified automatically.
We will use an ObservableCollection that will be cleared and then loaded with speakers. We will use an ObservableCollection because it has built in support for CollectionChanged events when we Add or Remove from it. This is very nice so we don't have to call OnPropertyChanged each time.
In the class above the constructor simply create an autoproperty:
public ObservableCollection<Speaker> Speakers { get; set; }
Inside of the constructor create a new instance of the ObservableCollection so the constructor looks like:
public SpeakersViewModel()
{
Speakers = new ObservableCollection<Speaker>();
}
We are now set to create our method called GetSpeakers, which will call to get all of the speaker data from the internet. We will first implement this with a simply HTTP request, but later on update it to grab and sync the data from Azure!
First, create a method called GetSpeakers which is of type async Task (it is a Task because it is using Async methods):
async Task GetSpeakers()
{
}
The following code will be written INSIDE of this method:
First is to check if we are already grabbind data:
async Task GetSpeakers()
{
if(IsBusy)
return;
}
Next we will create some scaffolding for try/catch/finally blocks:
async Task GetSpeakers()
{
if (IsBusy)
return;
Exception error = null;
try
{
IsBusy = true;
}
catch (Exception ex)
{
error = ex;
}
finally
{
IsBusy = false;
}
}
Notice, that we set IsBusy to true and then false when we start to call to the server and when we finish.
Now, we will use HttpClient to grab a the json from the server inside of the try block.
using(var client = new HttpClient())
{
//grab json from server
var json = await client.GetStringAsync("http://demo4404797.mockable.io/speakers");
}
Still inside of the using, we will Deserialize the json and turn it into a list of Speakers with Json.NET:
var items = JsonConvert.DeserializeObject<List<Speaker>>(json);
Still inside of the using, we can will clear the speakers and then load them into the ObservableCollection:
Speakers.Clear();
foreach (var item in items)
Speakers.Add(item);
If anything goes wrong the catch will save out the exception and AFTER the finally block we can pop up an alert:
if (error != null)
await Application.Current.MainPage.DisplayAlert("Error!", error.Message, "OK");
The completed code should look like:
async Task GetSpeakers()
{
if (IsBusy)
return;
Exception error = null;
try
{
IsBusy = true;
using(var client = new HttpClient())
{
//grab json from server
var json = await client.GetStringAsync("http://demo4404797.mockable.io/speakers");
//Deserialize json
var items = JsonConvert.DeserializeObject<List<Speaker>>(json);
//Load speakers into list
Speakers.Clear();
foreach (var item in items)
Speakers.Add(item);
}
}
catch (Exception ex)
{
Debug.WriteLine("Error: " + ex);
error = ex;
}
finally
{
IsBusy = false;
}
if (error != null)
await Application.Current.MainPage.DisplayAlert("Error!", error.Message, "OK");
}
Our main method for gettering data is now complete!
Intead of invoking this method directly, we will expose it with a Command. A Command has an interface that knows what method to invoke and has an optional way of describing if the Command is enabled.
Where we created our ObservableCollection<Speaker> Speakers {get;set;} create a new Command called GetSpeakersCommand:
public Command GetSpeakersCommand { get; set; }
Inside of the SpeakersViewModel constructor we can create the GetSpeakersCommand and assign it a method to use. We can also pass in an enabled flag leveraging our IsBusy:
GetSpeakersCommand = new Command(
async () => await GetSpeakers(),
() => !IsBusy);
The only modification that we will have to make is when we set the IsBusy property, as we will want to re-evaluate the enabled function that we created. In the set of IsBusy simply invoke the ChangeCanExecute method on the GetSpeakersCommand such as:
set
{
busy = value;
OnPropertyChanged();
//Update the can execute
GetSpeakersCommand.ChangeCanExecute();
}
It is now finally time to build out our first Xamarin.Forms user interface in the *View/SpeakersPage.xaml**
For the first page of the app we will add a few controls onto the page that are stacked vertically. We can use a StackLayout to do this. Inbetween the ContentPage add the following:
<StackLayout Spacing="0">
</StackLayout>
This will be the base where all of the child controls will go and will have no space inbetween them.
Next, let's add a Button that has a binding to the GetSpeakersCommand that we created:
<Button Text="Sync Speakers" Command="{Binding GetSpeakersCommand}"/>
Under the button we can display a loading bar when we are gathering data form the server. We can use an ActivityIndicator to do this and bind to the IsBusy property we created:
<ActivityIndicator IsRunning="{Binding IsBusy}" IsVisible="{Binding IsBusy}"/>
We will use a ListView that binds to the Speakers collection to display all of the items. We can use a special property called x:Name="" to name any control:
<ListView x:Name="ListViewSpeakers"
ItemsSource="{Binding Speakers}">
<!--Add ItemTemplate Here-->
</ListView>
We still need to describe what each item looks like, and to do so, we can use an ItemTemplate that has a DataTemplate with a specific View inside of it. Xamarin.Forms contains a few default Cells that we can use, and we will use the ImageCell that has an image and two rows of text
Replace with:
<ListView.ItemTemplate>
<DataTemplate>
<ImageCell Text="{Binding Name}"
Detail="{Binding Title}"
ImageSource="{Binding Avatar}"/>
</DataTemplate>
</ListView.ItemTemplate>
Xamarin.Forms will automatically download, cache, and display the image from the server.
Open the App.cs file and you will see the entry point for the application, which is the constructor for App(). It simply creates the new SpeakersPage, and then wraps it in a navigation page to get a nice title bar.
Now, you can set the iOS, Android, or UWP (Windows/VS2015 only) as the start project and start debugging.
If you are on a PC then you will need to be connected to a macOS device with Xamarin installed to run and debug the app.
If connected, you will see a Green connection status. Select iPhoneSimulator as your target, and then select the Simulator to debug on.
Simply set the DevDaysSpeakers.Droid as the startup project and select a simulator to run on.
Ensure that you have the SQLite extension installed for UWP apps:
Go to Tools->Extensions & Updates
Under Online search for SQLite and ensure that you have SQlite for Univeral Windows Platform installed (current version 3.13.0)
Simply set the DevDaysSpeakers.UWP as the startup project and select debug to Local Machine.
Now, let's do some navigation and display some Details. Let's upen up the code behind for SpeakersPage.xaml called SpeakersPage.xaml.cs.
In the code behind you will find the setup for the SpeakersViewModel. Under BindingContext = vm;, let's add an event to the ListViewSpeakers to get notified when an item is selected
ListViewSpeakers.ItemSelected += ListViewSpeakers_ItemSelected;
Let's create and fill in this method and navigate to the DetailsPage.
private async void ListViewSpeakers_ItemSelected(object sender, SelectedItemChangedEventArgs e)
{
var speaker = e.SelectedItem as Speaker;
if (speaker == null)
return;
await Navigation.PushAsync(new DetailsPage(speaker));
ListViewSpeakers.SelectedItem = null;
}
In the above code we check to see if the selected item is not null and then use the built in Navigation API to push a new page and then deselect the item.
Let's now fill in the Details page. Similar to the SpeakersPage, we will use a StackLayout, but we will wrap it in a ScrollView, incase we have long text.
<ScrollView Padding="10">
<StackLayout Spacing="10">
<!-- Detail controls here -->
</StackLayout>
</ScrollView>
Now, let's add controls and bindings for the properties in the Speaker object:
<Image Source="{Binding Avatar}" HeightRequest="200" WidthRequest="200"/>
<Label Text="{Binding Name}" FontSize="24"/>
<Label Text="{Binding Title}" TextColor="Purple"/>
<Label Text="{Binding Description}"/>
Now, for fun, let's add two buttons that we will add click events to in the code behind:
<Button Text="Speak" x:Name="ButtonSpeak"/>
<Button Text="Go to Website" x:Name="ButtonWebsite"/>
If we open up DetailsPage.xaml.cs we can now add a few more click handlers. Let's start with ButtonSpeak, where we will use the Text To Speech Plugin to read back the speaker's description.
In the constructor, add a click handler below the BindingContext:
ButtonSpeak.Clicked += ButtonSpeak_Clicked;
Then we can add the click handler and call the cross platform API for text to speech:
private void ButtonSpeak_Clicked(object sender, EventArgs e)
{
CrossTextToSpeech.Current.Speak(this.speaker.Description);
}
Xamarin.Forms itself has some nice APIs built write in for cross platform functionality, such as opening a URL in the default browser.
Let's add another click handler, but this time for ButtonWebsite:
ButtonWebsite.Clicked += ButtonWebsite_Clicked;
Then, we can use the Device keyword to call the OpenUri method:
private void ButtonWebsite_Clicked(object sender, EventArgs e)
{
if (speaker.Website.StartsWith("http"))
Device.OpenUri(new Uri(speaker.Website));
}
Now, we should be all set to compile, and run just like before!
Of course being able grab data from a RESTful end point is great, but what about a full backed, this is where Azure Mobile Apps comes in. Let's upgrade our application to use Azure Mobile Apps backend.
Head to http://portal.azure.com and register for an account.
Once you are in the portal select the + New button and search for mobile apps and you will see the results as follows, and we want to select Mobile Apps Quickstart
The Quickstart blade will open, select Create
This will open a settings blade with 4 settings:
App name
This is a unique name for the app that you will need when you configure the backend in your app. You may want to do somethign like yourlastnamespeakers or somethign like this.
Subscription Select a subscription or creat a pay as you go (this service will not cost you anything)
Resource Group Select Create new and call it DevDaysSpeakers
A resource group is a group of related services that can be easily deleted later.
App Service plan/Location Click this field and select Create New, give it a unique name, select a location (I am West US for intstance) and then select the F1 Free tier:
Finally check Pin to dashboard and click create:
This will take about 3-5 minutes to setup, so let's head back to the code!
We will be using the Azure App Service Helpers library that we saw earlier in the presentations to add an Azure backend to our mobile app in just four lines of code.
In the DevDaysSpeakers/App.cs file let's add a static property above the constructor for the Azure Client:
public static IEasyMobileServiceClient AzureClient { get; set; }
In the constructor simply add the following lines of code to create the client and register the table:
AzureClient = EasyMobileServiceClient.Create();
AzureClient.Initialize("https://YOUR-APP-NAME-HERE.azurewebsites.net");
AzureClient.RegisterTable<Model.Speaker>();
AzureClient.FinalizeSchema();
Be sure to udpate YOUR-APP-NAME-HERE with the app name you just specified.
Back in the ViewModel, we can add another private property to get a reference for the table. Above the constructor add:
ITableDataStore<Speaker> table;
Inside of the constructor use the statis AzureClient to get the table:
table = App.AzureClient.Table<Speaker>();
Now, instead of calling the HttpClient to get a string, let's query the Table:
Change the try block of code to:
try
{
IsBusy = true;
var items = await table.GetItemsAsync();
Speakers.Clear();
foreach (var item in items)
Speakers.Add(item);
}
Now, we have implemented all of the code we need in our app! Amazing isn't it! That's it! App Service Helpers will automatically handle all communication with your Azure backend for you, do online/offline synchronization so your app works even when it's not connected, and even automatic conflict resolution. Just 7 lines of code!
Let's head back to the Azure Portal and populate the database.
When the Quickstart finished you should see the following screen, or can go to it from tapping the pin on the dashboard:
Under Features select Easy Tables.
It will have created a TodoItem, which you should see, but we can create a new table and upload a default set of data by selecting Add from CSV from the menu.
Ensure that you have downloaded this repo and have the Speaker.csv file that is in this folder.
Select the file and it will add a new table name and find the fields that we have listed. Then Hit Start Upload.
Now you can re-run your application and get data from Azure!
Take Dev Days farther with these additional challenges that you can complete at home after Dev Days ends.
For fun, you can add the Cognitive Serivce Emotion API and add another Button to the detail page to analyze the speakers face for happiness level.
Go to: http://microsoft.com/cognitive and create a new account and an API key for the Emotion service.
Follow these steps:
1.) Add Microsoft.ProjectOxford.Emotion nuget package to all projects
2.) Add a new class called EmotionService and add the following code:
public class EmotionService
{
private static async Task<Emotion[]> GetHappinessAsync(string url)
{
var client = new HttpClient();
var stream = await client.GetStreamAsync(url);
var emotionClient = new EmotionServiceClient(CognitiveServicesKeys.Emotion);
var emotionResults = await emotionClient.RecognizeAsync(stream);
if (emotionResults == null || emotionResults.Count() == 0)
{
throw new Exception("Can't detect face");
}
return emotionResults;
}
//Average happiness calculation in case of multiple people
public static async Task<float> GetAverageHappinessScoreAsync(string url)
{
Emotion[] emotionResults = await GetHappinessAsync(url);
float score = 0;
foreach (var emotionResult in emotionResults)
{
score = score + emotionResult.Scores.Happiness;
}
return score / emotionResults.Count();
}
public static string GetHappinessMessage(float score)
{
score = score * 100;
double result = Math.Round(score, 2);
if (score >= 50)
return result + " % :-)";
else
return result + "% :-(";
}
}
3.) Now add a new button to the Details Page and expose it with x:Name="ButtonAnalyze
4.) Add a new click handler and add the async keyword to it.
5.) Call
var level = await EmotionService.GetAverageHappinessScoreAsync(this.speaker.Avatar);
6.) Then display a pop up alert:
await DisplayAlert("Happiness Level", level, "OK");
In this challenge we will make the speakers Title editable.
Open DetailsPage.xaml and change the Label that is displaying the Title from:
<Label Text="{Binding Title}" TextColor="Purple"/>
to an Entry with a OneWay databinding (this means when we enter text it will not change the actual data), and a Name to expose it in the code behind.
<Entry Text="{Binding Title, Mode=OneWay}"
TextColor="Purple"
x:Name="EntryTitle"/>
Let's add a save Button under the Go To Website button.
<Button Text="Save" x:Name="ButtonSave"/>
Open up SpeakersViewModel and add a new method called UpdateSpeaker(Speaker speaker), that will update the speaker , sync, and refresh the list:
public async Task UpdateSpeaker(Speaker speaker)
{
await table.UpdateAsync(speaker);
await table.Sync();
await GetSpeakers();
}
Let's update the constructur to pass in the SpeakersViewModel for the DetailsPage:
Before:
Speaker speaker;
public DetailsPage(Speaker item)
{
InitializeComponent();
this.speaker = item;
...
}
After:
Speaker speaker;
SpeakersViewModel vm;
public DetailsPage(Speaker item, SpeakersViewModel viewModel)
{
InitializeComponent();
this.speaker = item;
this.vm = viewModel;
...
}
Under the other click handlers we will add another click handler for ButtonSave.
ButtonSave.Clicked += ButtonSave_Clicked;
When the button is clicked, we will update the speaker, and call save and then navigate back:
private async void ButtonSave_Clicked(object sender, EventArgs e)
{
speaker.Title = EntryTitle.Text;
await vm.UpdateSpeaker(speaker);
await Navigation.PopAsync();
}
Finally, we will need to pass in the ViewModel when we navigate in the SpeakersPage.xaml.cs in the ListViewSpeakers_ItemSelected method:
//Pass in view model now.
await Navigation.PushAsync(new DetailsPage(speaker, vm));
There you have it!