In this blog post, I'll explain how to create a simple Twitter client with Xamarin Forms and .NET Standard 2.0. You can download the source code of the app from my GitHub repository here.

Known Issues

The UWP flavor of the app crashes, at the moment of this writing, because of this known bug of Xamarin.Auth. The iOS version of the app is not tested.

If you encounter the following error or random deployment errors, you need to delete bin and obj folder of the project and redeploy. Running Visual Studio 2017 as an admin may help.

Could not locate c:\temp\IC6.Xwitter\IC6.Xwitter\packages.config. Ensure that this project has Microsoft.Bcl.Build installed and packages.config is located next to the project file. 2 IC6.Xwitter.Android

Create a New Solution

We start with a brand new Visual Studio Solution to create a Xamarin.Forms app from the File-> New Project menu.

Cross-platform. Cross-platform app (Xamarin Forms). We choose a name. Hit OK.

We set blank app, all the platforms, Xamarin.Forms as the UI Technology, and .NET Standard as the Code Sharing Strategy.

Reorganize Some Files

We now have a blank app ready to be developed. We reorganize some files in the shared library to reflect the MVVM architecture of our app. We create the Models, ViewModels and Views folders.

We move the MainPage.xaml with drag and drop into the Views folder because the MainPage is a view, too.

Now we have to move the MainPage class in the correct namespace: IC6.Xwitter.Views.

The last step for this section is to provide the right namespace in the bootstrap of the app as well.

Add NuGet Packages

Xamarin.Auth

To make calls to the Twitter APIs we need to perform OAuth authentication. We can add the Xamarin.Auth NuGet package to avoid the implementation of OAuth. The Xamarin.Auth library provides a specific implementation for every platform because, for example, it needs to open a browser to perform the steps required by the OAuth standard. Because of that, we need to add this package to every project in our solution. We right-click the solution in the Solution Explorer and then click on Manage NuGet Packages for Solution: now we search and add the Xamarin.Auth package for all projects.

The next step is to configure initialize this library in every platform specific project because, as we said earlier, it has a specific implementation for every OS.

Android

For the Android project, we edit the MainActivity.cs file and we add this line of code:

global::Xamarin.Auth.Presenters.XamarinAndroid.AuthenticationConfiguration.Init(this, bundle);

iOS

For the iOS project, the concept is the same but we edit the AppDelegate.cs class with this line of code:

global::Xamarin.Auth.Presenters.XamarinIOS.AuthenticationConfiguration.Init();

UWP

The same concept is valid for the UWP world with this line of code in the App.xaml.cs file.

Xamarin.Auth.Presenters.UWP.AuthenticationConfiguration.Init();

Linq to Twitter

To avoid the heavy lifting of making the Twitter API calls we need the help of another library that enables us to access Twitter data like a database context: this library is very powerful and well done and it's called Linq to Twitter. The .NET Standard version of this library is not available as a NuGet package at the moment of this writing, but we can compile it by cloning the repository and building the .Net Standard project. I did this and the library is available here for download. Now we add a reference to this library into the shared project. We right-click the project, Add -> Reference -> Browse and we navigate to the file system location where we compiled or downloaded the library.

After we add the library, we click OK. The final result is something like the following image.

Warning! Important Step!

Now we try to build our solution to check if everything is fine. If we run into this error, we need to manually add another NuGet Package to all the projects of the solution: PCLCrypto (it is a dependency of Xamarin.Auth). The same may apply for Microsoft.Bcl.Build.

If we try to compile it again, everything should be fine now.

Views

Now it's time to code!

We start with the UI and we open the MainPage.xaml file in the Views folder of the shared library project. We write the XAML to achieve this layout.

We want to display our timeline as a list of tweets with the text, the author, and the profile image.

At the bottom, we want a place to input some text to do a tweet and then press a button to publish it.

<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:IC6.Xwitter" x:Class="IC6.Xwitter.Views.MainPage"> <StackLayout Margin="5"> <ListView ItemsSource="{Binding Tweets}" IsPullToRefreshEnabled="True" RefreshCommand="{Binding RefreshTimeline}" IsRefreshing="{Binding IsRefreshing}"> <ListView.ItemTemplate> <DataTemplate> <ImageCell Text="{Binding Text}" Detail="{Binding ScreenName}" ImageSource="{Binding ImageUrl}" /> </DataTemplate> </ListView.ItemTemplate> </ListView> <StackLayout Orientation="Horizontal" Margin="5" HorizontalOptions="FillAndExpand"> <Editor Text="{Binding NewTweetText}" HorizontalOptions="FillAndExpand" /> <Button Command="{Binding SendTweet}" Text="Send tweet" /> </StackLayout> </StackLayout> </ContentPage>





Model

We create a Tweet class in the Model folder of the shared project to represent a tweet with some basic properties.

namespace IC6.Xwitter.Models { public class Tweet { public ulong StatusID { get; set; } public string ScreenName { get; set; } public string Text { get; set; } public string ImageUrl { get; set; } } }

ViewModel

Now we create the ViewModel for the view. We create a new class in the ViewModels folder of the shared library called MainPageViewModel.cs.

To get the consumer key and the consumer secret for your app, we need to register the application in the app portal of Twitter.

using IC6.Xwitter.Models; using LinqToTwitter; using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; using Xamarin.Forms; namespace IC6.Xwitter.ViewModels { internal class MainPageViewModel : INotifyPropertyChanged { public Command _authenticateCommand; private readonly ILoginStore _loginStoreService; private IAuthorizer _auth; private ILinqToTwitterAuthorizer _authSvc; private bool _isRefreshing; private string _newTweetText; private Command _refreshTimeline, _sendTweet; private List<Tweet> _tweets; private UserSecrets _userSecrets; private string consumerKey = "rhYsgslO2JWW120sPCepSq6Uq"; private string consumerSecret = "vJNFjYdCv8HO0M6mI8UTcqWdgdR9qBOEDXcxmgtV20ZjMOeZwW"; public MainPageViewModel(ILoginStore loginStoreSvc, ILinqToTwitterAuthorizer authorizeSvc) { _loginStoreService = loginStoreSvc; _userSecrets = loginStoreSvc.GetSecrets(); _authSvc = authorizeSvc; if (_userSecrets != null) { _auth = _authSvc.GetAuthorizer(consumerKey, consumerSecret, _userSecrets.OAuthToken, _userSecrets.OAuthSecret); } } public event PropertyChangedEventHandler PropertyChanged; public Command Authenticate { get { return _authenticateCommand ?? ( _authenticateCommand = new Command(() => { InitAuthentication(); }) ); } } public bool IsAuthenticated { get { return _userSecrets != null; } } public bool IsRefreshing { get { return _isRefreshing; } private set { _isRefreshing = value; OnPropertyChanged(); } } public string NewTweetText { get { return _newTweetText; } set { if (value.Equals(_newTweetText, StringComparison.CurrentCulture)) return; _newTweetText = value; PropertyChanged(this, new PropertyChangedEventArgs(nameof(NewTweetText))); SendTweet.ChangeCanExecute(); } } public Command RefreshTimeline { get { if (_refreshTimeline == null) { _refreshTimeline = new Command(async () => { IsRefreshing = true; try { await RefreshAsync(); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex.Message); } IsRefreshing = false; }, () => { return _loginStoreService.GetSecrets() != null; }); } return _refreshTimeline; } } public Command SendTweet { get { if (_sendTweet == null) { _sendTweet = new Command( async () => { using (var ctx = new TwitterContext(_auth)) { await ctx.TweetAsync(NewTweetText); } NewTweetText = ""; RefreshTimeline.Execute(null); }, () => { return NewTweetText?.Length > 0 && _auth != null; }); } return _sendTweet; } } public List<Tweet> Tweets { get { return _tweets; } set { if (_tweets == value) return; _tweets = value; OnPropertyChanged(); } } public void InitAuthentication() { if (_userSecrets != null) return; var oauth = new Xamarin.Auth.OAuth1Authenticator(consumerKey, consumerSecret, new Uri("https://api.twitter.com/oauth/request_token"), new Uri("https://api.twitter.com/oauth/authorize"), new Uri("https://api.twitter.com/oauth/access_token"), new Uri("http://127.0.0.1/")); oauth.Completed += Oauth_Completed_GetAuthorizer; var presenter = new Xamarin.Auth.Presenters.OAuthLoginPresenter(); presenter.Login(oauth); } protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { if (propertyName == null) throw new ArgumentNullException("Can't call OnPropertyChanged with a null property name.", propertyName); PropertyChangedEventHandler propChangedHandler = PropertyChanged; if (propChangedHandler != null) propChangedHandler(this, new PropertyChangedEventArgs(propertyName)); } private async void Oauth_Completed_GetAuthorizer(object sender, Xamarin.Auth.AuthenticatorCompletedEventArgs e) { _auth = _authSvc.GetAuthorizer(consumerKey, consumerSecret, e.Account.Properties["oauth_token"], e.Account.Properties["oauth_token_secret"]); await _loginStoreService.SetSecretsAsync( e.Account.Properties["oauth_token"], e.Account.Properties["oauth_token_secret"] ); RefreshTimeline.ChangeCanExecute(); RefreshTimeline.Execute(null); } private async Task RefreshAsync() { await _auth.AuthorizeAsync(); using (var ctx = new TwitterContext(_auth)) { var srch = await (from tweet in ctx.Status where tweet.Type == StatusType.Home select new Tweet() { StatusID = tweet.StatusID, ScreenName = tweet.User.ScreenNameResponse, Text = tweet.Text, ImageUrl = tweet.RetweetedStatus != null && tweet.RetweetedStatus.User != null ? tweet.RetweetedStatus.User.ProfileImageUrl.Replace("http://", "https://") : tweet.User.ProfileImageUrl }).ToListAsync(); Tweets = new List<Tweet>(srch); } } } }

View-Model Deep Dive

The above View-Model has some pieces of code that need explanation.

The Constructor

As we can see, the constructor needs two parameters. The first is a reference to a service that manages the persistence of the user login information to avoid a new login everytime we open the app. The second is a reference to a service of the LinqToTwitter library to handle the exchange of the user's token when accessing the Twitter API.

The ILoginStore and ILinqToTwitterAuthorizer interfaces are declared in the shared library in the ILoginStore.cs file and ILinqToTwitterAuthorizer.cs file.

using System; using System.Collections.Generic; using System.Text; namespace IC6.Xwitter { public interface ILoginStore { void SetSecrets(UserSecrets secrets); void SetSecrets(string oauthToken, string oauthSecret); UserSecrets GetSecrets(); } }

using System.Threading.Tasks; namespace IC6.Xwitter { public interface ILoginStore { UserSecrets GetSecrets(); Task SetSecretsAsync(UserSecrets secrets); Task SetSecretsAsync(string oauthToken, string oauthSecret); } }

The UserSecret class is declared in the UserSecret.cs file in the root of the shared project.

namespace IC6.Xwitter { public class UserSecrets { public string OAuthSecret { get; set; } public string OAuthToken { get; set; } } }

The LoginStore class and the LinqToTwitterAuthorizer class are declared in the root of the shared library with the LoginStore.cs and LinqToTwitterAuthorizer.cs names respectively. The LoginStore class leverages the Xamarin App class to get and set user preferences. The LinqToTwitterAuthorizer class uses base components of the LinqToTwitter API to handle the authorization needed by the Twitter API.

using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; namespace IC6.Xwitter { class LoginStore : ILoginStore { private enum OAuth { OAuthToken, OAuthSecret } public UserSecrets GetSecrets() { string token = null; string secret = null; if (App.Current.Properties.ContainsKey(OAuth.OAuthToken.ToString())) { token = App.Current.Properties[OAuth.OAuthToken.ToString()].ToString(); } if (string.IsNullOrEmpty(token)) { return null; } if (App.Current.Properties.ContainsKey(OAuth.OAuthSecret.ToString())) { secret = App.Current.Properties[OAuth.OAuthSecret.ToString()].ToString(); } if (string.IsNullOrEmpty(secret)) { return null; } return new UserSecrets() { OAuthSecret = secret, OAuthToken = token }; } public async Task SetSecretsAsync(UserSecrets secrets) { await SetSecretsAsync(secrets.OAuthToken, secrets.OAuthSecret); } public async Task SetSecretsAsync(string oauthToken, string oauthSecret) { App.Current.Properties[OAuth.OAuthSecret.ToString()] = oauthSecret; App.Current.Properties[OAuth.OAuthToken.ToString()] = oauthToken; await App.Current.SavePropertiesAsync(); } } }

using LinqToTwitter; namespace IC6.Xwitter { public class LinqToTwitterAuthorizer : ILinqToTwitterAuthorizer { public IAuthorizer GetAuthorizer(string consumerKey, string consumerSecret) { return new ApplicationOnlyAuthorizer() { CredentialStore = new InMemoryCredentialStore { ConsumerKey = consumerKey, ConsumerSecret = consumerSecret, }, }; } public IAuthorizer GetAuthorizer(string consumerKey, string consumerSecret, string oAuthToken, string oAuthTokenSecret) { var cred = new InMemoryCredentialStore { ConsumerKey = consumerKey, ConsumerSecret = consumerSecret, OAuthToken = oAuthToken, OAuthTokenSecret = oAuthTokenSecret }; var auth0 = new PinAuthorizer() { CredentialStore = cred, }; return auth0; } } }

TL;DR

In this post, we developed a simple Twitter client that shows the user timeline and can send tweets. Some libraries helped us to avoid the hard work of the OAuth standard and the Twitter API.

References

For more information about the library used in this post, you can read the docs in the pages of the projects.