I'm fetching tweets from the Twitter API and display them in a TextBlock
using multiple Inline
elements so that I can highlight (and link) entities embedded in the tweet: hashtags
, urls
and user_mentions
. media
entities are currently ignored.
Tweets also usually contains emojis. From the API they are returned as two unicode characters. Even though they are displayed properly in the TextBlock
, due to the nature of C#, the fact that they consist of (at least) two characters, the indices in the tweets are "off" when trying to locate the entities in the tweet because the indices count emojis as one character, and C# counts them as two.
Because I'm using MVVM, and there's no way of binding to the inlines in a TextBlock, I have created a dependency property that I can bind to. Through an IValueConverter
I can bind a tweet to a TextBlock, and the converter creates inlines, which the dependency property puts into the TextBlock.
I have managed to display tweets correctly, but there are quite a few checks and balances to handle when looping through the entities and creating the inlines, so I would really appreciate some input on how (if?) this can be handled more efficiently.
So I present to you, my ValueConverter
public class TweetToTextblockConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
Tweet tweet = value as Tweet;
// Tweet is my wrapper class for tweets from Twitter API.
// It contains the same property values as the API object, but in a
// more properly named fashion
if (tweet == null) return value;
List<Inline> textList = new List<Inline>();
if (tweet.Entities.Any())
{
// Unicode emojis take up two characters in a string, and are counted as two,
// so combine all unicode characters to one character, and get old vs. new
// indices, and use this as a lookup when adding entities
int[] textIndices = StringInfo.ParseCombiningCharacters(tweet.Text);
int prevIndex = 0;
foreach (Entity entity in tweet.Entities.OrderBy(e => e.StartIndex))
{
// Display text between the current and previous entity
// (or start of tweet text)
int length = textIndices[entity.StartIndex] - prevIndex;
if (length < 0) length = 0;
prevIndex = prevIndex > tweet.Text.Length ? tweet.Text.Length : prevIndex;
string subText = tweet.Text.Substring(prevIndex, length);
textList.Add(new Run(subText));
// Add a link to the entity
Hyperlink hyperlink = new Hyperlink
{
NavigateUri = entity.Uri,
ToolTip = entity.Tooltip,
BaselineAlignment = BaselineAlignment.Center
};
hyperlink.Inlines.Add(entity.DisplayUri);
hyperlink.RequestNavigate += Hyperlink_RequestNavigate;
textList.Add(hyperlink);
// Save the end index of the entity so that the next run through the
// loop can display the text between this entity and the next one
if (entity.EndIndex > textIndices.Length)
{
prevIndex = textIndices.Length - 1;
}
else
{
prevIndex = textIndices[entity.EndIndex - 1] + 1;
}
}
// Add the last text that will not be added by the above loop
int finalLength = tweet.Text.Length - prevIndex;
if (finalLength < 0) finalLength = 0;
prevIndex = prevIndex > tweet.Text.Length ? tweet.Text.Length : prevIndex;
string finalSubText = tweet.Text.Substring(prevIndex, finalLength);
textList.Add(new Run(finalSubText));
}
else
{
textList.Add(new Run(tweet.Text));
}
return textList;
}
private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)
{
// This will open inline hyperlinks in the user's default browser
Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri));
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}