4
\$\begingroup\$

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();
    }
}
\$\endgroup\$
2
\$\begingroup\$

There are two code blocks that are basically identical. This

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);

and that

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);

You should try to encapsulate this logic otherwise if you need to change anything later you'll need to think to do it twice.


Alternatively you can attach the item that you process outside the loop to the entity collection and let the loop do the same job for it too:

tweet.Entities.OrderBy(e => e.StartIndex).Concat(new [] { Entity { ... } })

this way you wouldn't have to copy/paste the repeated logic. And if it doesn't have a link or an URL then it's easier to add an if to check it then duplicating the code.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.