SilverTweet – Building a Silverlight Twitter client part 5

I’m finally back after a couple of weeks with too much stuff to do. It is definitely time to finish off the blog series about SilverTweet, the Silverlight twitter client that you can build on your own. Hopefully you can then take this thing and extend it with the features you need.

For those of you who haven’t read the previous parts, I would recommend doing so. Otherwise, this part will give you just about nothing valuable. And for those of you who have, and did so before I posted this part, I just want to mention that I found a pretty obvious bug in part 3.

The example code in this post will create a functional, but fugly UI. The downloadable code will contain this UI, as well as an extra UI with better layout, some animations and so on. The reason for the fugly UI here is simplicity. Not that the other UI is REALLY beautiful, but it is at least a lot better…

But let’s just get started straight away and have a look at the UI that we need to start using the application.

The first thing to take care of is the MainPage.xaml file. This will be the main part of the application. It will use a simple little “navigation” solution based on something I have added to every model that will be used. Every single model that is being used in this application has a name. And every model will have it’s own user control to make up the view. So the MainPage control, is basically just responsible for making sure that the correct user control is displayed together with the correct model. As you might remember, the MainModel had a property called CurrentView, that was used to determine what “view” we were at. So to make sure that the correct user control is being shown together with the current view, I have created a DependencyProperty called CurrentView in the MainPage control.

public partial class MainPage : UserControl
{
private static DependencyProperty CurrentViewProperty =
DependencyProperty.Register("CurrentWiew", typeof(string), typeof(MainPage),
new PropertyMetadata(null, CurrentWiewChanged));

public MainPage()
{
InitializeComponent();
...
}

public static void CurrentWiewChanged(object sender, DependencyPropertyChangedEventArgs e)
{
((MainPage)sender).OnCurrentWiewChanged((string)e.NewValue);
}

...

public string CurrentWiew
{
get { return (string)GetValue(CurrentViewProperty); }
set { SetValue(CurrentViewProperty, value); }
}
}

As you can see, there is nothing complicated going on here yet. It is just a simple DependencyProperty of type string that is called CurrentView. It uses a callback function to get notified of any changes. In the callback, which has to be static, the new value is forwarded to an instance method for handling. There are a few things that I have left out in this code. In the constructor, I wire up a binding that will bind the CurrentView property to the Name of the CurrentView in the MainModel. This causes the CurrentView property to change as soon as the CurrentView in the MainModel is changed

public MainPage()
{
InitializeComponent();
this.SetBinding(CurrentViewProperty, new System.Windows.Data.Binding("CurrentView.Name"));
}

 

The static callback method then forwards the change notification to an instance method that handles the functionality. That instance method is called OnCurrentWiewChanged and takes the new view name as a parameter. It clears the “LayoutRoot’s” children and then instantiate the correct user control based on the view’s name. It then adds the control to the “LayoutRoot’s” child collection. It also makes sure to set the DataContext of the control. Very simple, but effective way of controlling what is shown in the UI from the ViewModel, without coupling them…

protected virtual void OnCurrentWiewChanged(string newView)
{
LayoutRoot.Children.Clear();
Control ctrl = null;
switch (newView)
{
case "Login":
ctrl = new LoginControl();
break;
case "CredentialVerification":
ctrl = new CredentialVerificationControl();
break;
case "Timeline":
ctrl = new TimelineControl();
break;
}
if (ctrl != null)
{
ctrl.DataContext = GetDataContextProperty("CurrentView");
LayoutRoot.Children.Add(ctrl);
}
}

Getting the DataContext to use for the control is however a little fiddly. We need to get the “CurrentView” property from our DataContext. This is not very hard, but requires a bit of reflection…

private object GetDataContextProperty(string propertyName)
{
object dContext = this.DataContext;
return dContext.GetType().GetProperty("CurrentView").GetValue(dContext, null);
}

That’s it. That’s all that we need in the MainPage.xaml.cs. The xaml as such is actually not changed at all

<UserControl x:Class="SilverTweet.Blog.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
<Grid x:Name="LayoutRoot">

</Grid>
</UserControl>

So, the next step would be to create the individual controls that is used to create the UI. Let’s start with the LoginControl. It essentially needs HyperlinkButton to send off the user to Twitter to get the PIN code, a TextBox to enter the PIN code in and a button to do the login. So I have taken these important controls and placed them in a Grid together with some information text on what to do. The visibility of this Grid is based on the ViewModel’s AuthorizationURL property. The Grid should only be visible when there is an URL to redirect the user to. The HyperlinkButton’s NavigateUri is also bound to this property. The TextBox and the Button is visible at the same time as the Grid, but their IsEnabled properties are bound to the ViewModel’s IsGettingAccessToken property, disabling them while getting the access token.

Finally, I have hooked up the Button’s Click event to the ViewModel’s VerifyPin command using the CommandManager. I pass the PIN to the command using an element binding. It all looks like this

<Grid Width="450" 
Visibility="{Binding AuthorizationURL, Converter={StaticResource StringConverter}}"
HorizontalAlignment="Center" VerticalAlignment="Center" >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>

<TextBlock TextWrapping="Wrap" HorizontalAlignment="Stretch" Text="Information text. Removed for simplicity..." />
<HyperlinkButton Grid.Row="1" NavigateUri="{Binding AuthorizationURL}" TargetName="_blank"
Content="Get PIN Code From Twitter" HorizontalAlignment="Center"
FontSize="15" FontWeight="Bold" Margin="0,10"/>
<TextBlock x:Name="VerificationErrorText" Grid.Row="2" Margin="0,0,0,15" Text="{Binding VerificationError}"
Visibility="{Binding VerificationError,Converter={StaticResource StringConverter}}" />
<TextBox IsEnabled="{Binding IsGettingAccessToken, Converter={StaticResource ReverseBoolConverter}}"
x:Name="PINCode" Grid.Row="3" HorizontalAlignment="Center" VerticalAlignment="Center" Width="140"
Height="40" FontSize="26.667" FontWeight="Bold" Padding="0" TextAlignment="Center"/>
<Button IsEnabled="{Binding IsGettingAccessToken, Converter={StaticResource ReverseBoolConverter}}"
x:Name="VerifyButton" Grid.Row="4" Content="Verify PIN" Margin="0,5,0,0"
cmd:CommandManager.CommandEventName="Click"
cmd:CommandManager.CommandParameter="{Binding Text, ElementName=PINCode}"
cmd:CommandManager.Command="{Binding VerifyPin}" />
</Grid>

 

However, there i a slight chance that there will be a communication error when working with this control. Therefore, the ViewModel exposes a string property called CommunicationError that indicates if a communication error has been caused. To make this visible to the user, I have added an extra grid to the control, and bound its visibility to the same property using a converter

<Grid Width="240" Visibility="{Binding CommunicationError, Converter={StaticResource StringConverter}}" 
      VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="{Binding CommunicationError}" />
</Grid>

That’s the entire login control. Ugly, but working.

As you saw in the xaml above, I am using a couple of converters to get it all to work. These are all declared in the App.xaml file, since they will be used by all controls. I will go through them as they are being used. In the previous code, I used a StringConverter as well as a ReverseBoolConverter. They are implemented as follows

public class StringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string str = (string)value;
if (targetType == typeof(Visibility))
{
return string.IsNullOrEmpty(str) ? Visibility.Collapsed : Visibility.Visible;
}
else if (targetType == typeof(Uri))
{
return new Uri(str);
}
else if (targetType == typeof(ImageSource))
{
return new BitmapImage(new Uri(str));
}
return value;
}

public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}

 

public class ReverseBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
bool b = (bool)value;
if (targetType == typeof(Visibility))
{
return b ? Visibility.Collapsed : Visibility.Visible;
}
return !b;
}

public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}

 

Not very complicated, but they get the job done nicely.

If the user has already logged in before and thus already have an AccessToken, the user is instead shown a control called CredentialVerificationControl. This control will be shown while the application tries to verify that the AccessToken is still valid. It consists of three “pieces”, one Grid and two StackPanels. The Grid contains a TextBlock that displays the current status. It’s visibility is controlled by a binding to the same property. No status = not visible.

<Grid HorizontalAlignment="Center" VerticalAlignment="Center" 
Visibility="{Binding Status, Converter={StaticResource StringConverter}}">
<TextBlock Text="{Binding Status}" TextAlignment="Center" x:Name="StatusText" />
</Grid>

The first StackPanel contains a TextBlock as well as a Button. Its visibility is controlled by a binding to the CommunicationError property. If there is a communication error, this is displayed to the user who then gets the option to retry the verification by clicking on the Button. The Button’s Click event is bound to the ViewModel’s VerifyCredentials command.

<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" 
Visibility="{Binding CommunicationError, Converter={StaticResource StringConverter}}">
<TextBlock Text="{Binding CommunicationError}" />
<Button Content="Retry" Margin="0,20,0,0"
cmd:CommandManager.CommandEventName="Click"
cmd:CommandManager.Command="{Binding VerifyCredentials}"/>
</StackPanel>

The final piece is more or less identical to the previous, but binds to the VerificationError property instead. And the Button is wired to the ReAuthenticate command instead.

<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"
Visibility="{Binding VerificationError, Converter={StaticResource StringConverter}}">
<TextBlock Text="{Binding VerificationError}" />
<Button Content="Login" Margin="0,20,0,0"
cmd:CommandManager.CommandEventName="Click"
cmd:CommandManager.Command="{Binding ReAuthenticate}"/>
</StackPanel>

 

There is one final thing though. At the moment, nothing will actually happen when this control is shown. I need a way to begin the verification process. This is easily handled using the CommandManager and the Loaded event of the control.

<UserControl
...
xmlns:cmd="clr-namespace:CommandManager;assembly=CommandManager"
cmd:CommandManager.CommandEventName="Loaded" cmd:CommandManager.Command="{Binding VerifyCredentials}">
...

 

The final “view” to create is the Timeline control. It is responsible for displaying the Twitter timeline to the user. It also contains controls that enable the user to create a tweet. It consists of a Grid that holds the 2 “pieces”, the timeline list and the tweet functionality. The timeline list is just a ListBox with its ItemsSource property bound to the Tweets property of the DataContext. However, it does need a DataTemplate to display the tweets in a useful way. The DataTemplate consists of a Grid with 2 columns. The first column contains an Image (with a dark gray border) and the second contains a StackPanel. The StackPanel contains a custom control called TweetTextBlock and another StackPanel. The nested StackPanel in turn contains TextBlocks displaying the screen name of the user who tweeted the tweet, and the time since the tweet was made. This sounds really complicated…it looks like this

<ListBox ItemsSource="{Binding Tweets}" Width="400" HorizontalAlignment="Center">
<ListBox.ItemTemplate>
<DataTemplate x:Name="TweetItemTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="60" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border BorderThickness="1" BorderBrush="DarkGray" VerticalAlignment="Top" HorizontalAlignment="Left" Width="50" Height="50">
<Image Source="{Binding By.ProfileImageUrl, Converter={StaticResource StringConverter}}" />
</Border>
<StackPanel Grid.Column="1">
<ctrl:TweetTextBlock Text="{Binding Text}" Width="310" />
<StackPanel Orientation="Horizontal">
<TextBlock FontSize="10" Text="By: " />
<TextBlock FontSize="10" Text="{Binding By.ScreenName}" />
<TextBlock FontSize="10" Text=" @ " />
<TextBlock FontSize="10" Text="{Binding CreatedLocal, Converter={StaticResource TimeConverter}}" />
</StackPanel>
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

 

I will go into detail about the TweetTextBlock control later, for now all you need to know is that it shows the tweet text. The code above also introduces a new converter, the TimeConverter. This converter takes the DateTime representing the creation time, and converts it to a relative time.

public class TimeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
DateTime dt = (DateTime)value;
if (targetType == typeof(string))
{
string format = "about {0} {1} ago";
TimeSpan ts = DateTime.Now - dt;
if (ts.Days > 0)
{
return string.Format(format,ts.Days,"days");
}
else if (ts.Hours > 0)
{
return string.Format(format, ts.Hours, "hours");
}
else
{
return string.Format(format, ts.Minutes, "minutes");
}
}
return value;
}

public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}

 

The second “piece” of the Timeline control is the tweet functionality. The UI for this is a StackPanel with a TextBox and a Button. The TextBox’ Text property is bound to the TweetText property using a TwoWay binding and the Button’s Click event is bound to the Tweet property. They both also have their IsEnabled property bound to the IsSendingTweet property. This will disable the controls while the tweet is being sent…

<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" 
Margin="0,10,0,0" Grid.Row="1">
<TextBox Width="285" Height="45" AcceptsReturn="True" TextWrapping="Wrap" Text="{Binding TweetText,Mode=TwoWay}"
IsEnabled="{Binding IsSendingTweet, Converter={StaticResource ReverseBoolConverter}}"
MaxLength="140"/>
<Button Content="Tweet!"
HorizontalAlignment="Right" VerticalAlignment="Center" Height="45"
IsEnabled="{Binding IsSendingTweet, Converter={StaticResource ReverseBoolConverter}}"
cmd:CommandManager.CommandEventName="Click"
cmd:CommandManager.Command="{Binding Tweet}"
Margin="5,0,0,0" />
</StackPanel>

 

That’s it…that is all the controls needed for the UI. I guess the only thing left to show is the TweetTextBlock. The TweetTextBlock control implements some of the Twitter text functionality. It converts links and @ commands to HyperlinkButtons. It is a templated control, which means that I have had to add a generic.xaml file in a themes folder in my project. The template for the control is very simple. However, it uses a WrapPanel which is currently only available in the Silverlight Toolkit.

<Style TargetType="local:TweetTextBlock">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:TweetTextBlock">
<Grid Background="{TemplateBinding Background}">
<toolkit:WrapPanel x:Name="RootPanel" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

 

The TweetTextBlock control exposes a single property, Text. This is a DependencyProperty, so that it is possible to bind it’s value. The foundation of the control looks like this

public class TweetTextBlock : Control
{
public static DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(TweetTextBlock),
new PropertyMetadata(null, OnTextChanged));

WrapPanel _rootPanel;
private const string TWITTER_USER_URL_FORMAT = "http://twitter.com/{0}";

public TweetTextBlock()
{
this.DefaultStyleKey = typeof(TweetTextBlock);
}

public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_rootPanel = GetTemplateChild("RootPanel") as WrapPanel;
UpdatePanelControls();
}

private static void OnTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
((TweetTextBlock)sender).OnTextChanged((string)e.NewValue);
}
protected virtual void OnTextChanged(string newString)
{
UpdatePanelControls();
}

private void UpdatePanelControls()
{
...
}

public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
}

 

As you might have noticed, there is nothing happening here. So, the UpdatePanelControls methods must be pretty crucial, and it is. It is responsible for creating the UI. It starts off by verifying that the _rootPanel isn’t null. Someone could have changed the template, so this is important to check. Next it clears any controls in the root panel. It then splits the Text property into words and loops through them. If the word starts with “http://” or “@” it creates a HyperlinkButton, otherwise it adds a TextBlock. If a word starts with “@”, it generates a suitable URL that will send the user to Twitter if clicked. The loop also adds a TextBlock containing an space between each of the controls. The WrapPanel will make sure that the controls wrap nicely and looks just like ordinary text…

private void UpdatePanelControls()
{
if (_rootPanel != null)
{
_rootPanel.Children.Clear();
string[] strs = Text.Split(' ');
for (int i = 0; i < strs.Length; i++)
{
string str = strs[i];
if (str.StartsWith("http://",StringComparison.InvariantCultureIgnoreCase))
{
_rootPanel.Children.Add(GetHyperlinkControl(str, str));
}
else if (str.StartsWith("@", StringComparison.InvariantCultureIgnoreCase))
{
string url = string.Format(TWITTER_USER_URL_FORMAT, str.Substring(1));
_rootPanel.Children.Add(GetHyperlinkControl(str, url));
}
else
{
_rootPanel.Children.Add(GetTextControl(str));
}

if (i < strs.Length - 1)
{
_rootPanel.Children.Add(GetSpaceControl());
}
}
}
}

 

The UIElement Get<Type>Control() methods are very simple and look like this

private UIElement GetSpaceControl()
{
return new TextBlock() { Text = " " };
}
private UIElement GetTextControl(string str)
{
return new TextBlock() { Text = str };
}
private UIElement GetHyperlinkControl(string text, string url)
{
HyperlinkButton btn = new HyperlinkButton()
{
Content = text,
NavigateUri=new Uri(url,UriKind.Absolute),
TargetName="_blank"
};
ToolTipService.SetToolTip(btn, url);
return btn;
}

Believe it or not, that is actually it. Once again, the UI takes the least time to build (very likely because it is ugly). But as I said, this UI has no real design and no animations to make it look good or anything. The alternative UI that is in the download contains a bit more design stuff and also some extra features to support transition animations and so on. But that is up to you to explore on your own.

SilverTweet.zip (2.04 mb)

If you have any questions, you know what to do…I hope…

Comments (4) -

Thanks a lot for this very detailed blog about the twitter client. Must have taken a lot of hours to write it. Will read it more carefully this evening and build twitter client with Silverlight 5.

Do I have to take care of crossdomain issues using silverlight in this way with twitter?

OK, missed the part about crossdomain in part 1, but see that you still need your own wcf service to connect to twitter. Such a pity that you cannot talk to twiiter directly from Silverligth application

Wondering if the direct connection Silverlight (running in browser)<-> twitter without the crossdomain issue would be possible in Silverligth 5, when you make the silverlight client a trusted application


see msdn.microsoft.com/.../ee721083%28VS.95%29.aspx

Relaxed Cross-Domain Access Restrictions

Trusted applications can perform networking and socket communication without being subject to cross-domain and cross-scheme access restrictions. For example, an application installed from one sub-domain using the HTTP protocol can access media files from a separate sub-domain using the HTTPS protocol. For more information about cross-domain communication, see HTTP Communication and Security with Silverlight.

Rob!
I think it should be. This code has been around for a while, but I suggest you give it a quick try in v 5...
// Chris

Pingbacks and trackbacks (1)+

Comments are closed