При разработке кроссплатформенного приложения встаёт вопрос об унификации функционала между разными платформами. Когда мы разрабатывали Edusty, мы столкнулись с неожиданной для нас проблемой — отсутствием встроенной функции автовыделения ссылок в тексте на платформах Windows/Windows Phone, которая присутствует на платформах Android и iOS. Более того — мы не нашли даже сторонних библиотек, реализующих этот функционал. Пришлось реализовывать этот функционал самим. О том, что получилось, будет рассказано в этой статье.
На странице, где необходимо отобразить текст со ссылками, расположен контрол RichTextBlock. Этот контрол не поддерживает MVVM-привязку данных, поэтому заполнять его пришлось «по старинке».
Заполнить RichTextBlock можно тремя способами:
1. Статичная XAML разметка прямо в коде страницы.
2. Программное заполнение коллекции BlockCollection .Blocks. Обычно она заполняется объектами типа Paragraph, которые инициализируются объектами, наследующими класс Inline (например, Run, Hyperlink и так далее).
3. Так же, как и во втором случае, заполнение коллеции Blocks, однако формирование объекта Paragraph происходит с помощью статичного класса XamlReader из XAML-разметки (сформированной в строкой форме).
В данном случае самым оптимальным будет третий метод, поскольку позволяет максимально гибко формировать разметку. Для того, чтобы распарсить xaml-строку в объекты, необходимо вызывать метод XamlReader.Load(xamlString). Данный метод возвращает object, который можно привести (в нашем случае) к типу Paragraph и добавить к RichTextBlock.Blocks.
И так, у нас есть входная строка, содержащая некий текст со ссылками или без них, а на выходе необходимо получить строку с валидной xaml разметкой для RichTextBlock (тег Paragraph), где все ссылки были бы в тегах Hyperlink, а обычный текст в тегах Run.
Для этого весь текст делится в массив слов по пробелам, затем выходная строка начинает формироваться таким образом, чтобы все теги всегда были закрыты при любой входной строке.
1. В самое начало текста добавляется незакрытый тег Run.
2. Запускается цикл по словам, где каждое слово будет проверяться с помощью регулярного выражения, является ли оно ссылкой или нет. Если является, то происходит закрытие тэга Run и вставка тега Hyperlink с соответствующей ссылкой, после чего снова открывается тег Run. Если же текущее слово не является ссылкой, то просто записываем данное слово к результату и переходим к следующему слову.
3. Когда все слова перебраны, то необходимо закрыть тег Run.
С обработкой ссылок не всё так просто, как может показаться на первый взгляд. Для начала определим, какие бывают ссылки: с указанием протокола, без него, с доменом, с ip адресом, с портом или без, с параметрами или без них.
В исходном тексте ссылки могут быть как уже URL-кодированы, так и нет. В последнем случае они могут содержать символы, из-за которых xaml разметка становится не валидной, поэтому ссылку необходимо обработать с помощью метода Uri.EscapeUriString(), который URL-кодирует только параметры ссылки, но не протокол, домен или порт. Однако это ещё не всё. URL-кодирование не заменяет символ '&', однако этот символ также делает xaml разметку не валидной, поэтому его следует заменять на его html-код '&аmp;'.
Также особенностью Windows-платформ является то, что чтобы открыть ссылку в другом приложении, ОС смотрит, какое приложение установлено по умолчанию для протокола, указанного в этой ссылке (например, httр://), поэтому, если протокол не указан, открыть такую ссылку ОС не может (более того, это даже вызовет исключение UriFormatException). Так что к любой ссылке, где не указан протокол, необходимо добавить по умолчанию протокол httр://.
Исходный текст иногда может содержать различные символы, нарушающие xaml разметку, поэтому его необходимо HTML-кодировать перед помещением в тег Run с помощью метода WebUtility.HtmlEncode.
После всего этого формируется новая строка, состоящая из тега Paragraph с соответствующими параметрами и содержащего в себе сформированный ранее набор тегов. Xaml разметка готова.
На странице, где необходимо отобразить текст со ссылками, расположен контрол RichTextBlock. Этот контрол не поддерживает MVVM-привязку данных, поэтому заполнять его пришлось «по старинке».
<RichTextBlock Margin="20" x:Name="RTB" FontSize="20"/>
Заполнить RichTextBlock можно тремя способами:
1. Статичная XAML разметка прямо в коде страницы.
2. Программное заполнение коллекции BlockCollection .Blocks. Обычно она заполняется объектами типа Paragraph, которые инициализируются объектами, наследующими класс Inline (например, Run, Hyperlink и так далее).
3. Так же, как и во втором случае, заполнение коллеции Blocks, однако формирование объекта Paragraph происходит с помощью статичного класса XamlReader из XAML-разметки (сформированной в строкой форме).
В данном случае самым оптимальным будет третий метод, поскольку позволяет максимально гибко формировать разметку. Для того, чтобы распарсить xaml-строку в объекты, необходимо вызывать метод XamlReader.Load(xamlString). Данный метод возвращает object, который можно привести (в нашем случае) к типу Paragraph и добавить к RichTextBlock.Blocks.
RTB.Blocks.Add((Paragraph)XamlReader.Load(xamlString));
Формирование XAML-строки
И так, у нас есть входная строка, содержащая некий текст со ссылками или без них, а на выходе необходимо получить строку с валидной xaml разметкой для RichTextBlock (тег Paragraph), где все ссылки были бы в тегах Hyperlink, а обычный текст в тегах Run.
Для этого весь текст делится в массив слов по пробелам, затем выходная строка начинает формироваться таким образом, чтобы все теги всегда были закрыты при любой входной строке.
1. В самое начало текста добавляется незакрытый тег Run.
2. Запускается цикл по словам, где каждое слово будет проверяться с помощью регулярного выражения, является ли оно ссылкой или нет. Если является, то происходит закрытие тэга Run и вставка тега Hyperlink с соответствующей ссылкой, после чего снова открывается тег Run. Если же текущее слово не является ссылкой, то просто записываем данное слово к результату и переходим к следующему слову.
3. Когда все слова перебраны, то необходимо закрыть тег Run.
С обработкой ссылок не всё так просто, как может показаться на первый взгляд. Для начала определим, какие бывают ссылки: с указанием протокола, без него, с доменом, с ip адресом, с портом или без, с параметрами или без них.
В исходном тексте ссылки могут быть как уже URL-кодированы, так и нет. В последнем случае они могут содержать символы, из-за которых xaml разметка становится не валидной, поэтому ссылку необходимо обработать с помощью метода Uri.EscapeUriString(), который URL-кодирует только параметры ссылки, но не протокол, домен или порт. Однако это ещё не всё. URL-кодирование не заменяет символ '&', однако этот символ также делает xaml разметку не валидной, поэтому его следует заменять на его html-код '&аmp;'.
Также особенностью Windows-платформ является то, что чтобы открыть ссылку в другом приложении, ОС смотрит, какое приложение установлено по умолчанию для протокола, указанного в этой ссылке (например, httр://), поэтому, если протокол не указан, открыть такую ссылку ОС не может (более того, это даже вызовет исключение UriFormatException). Так что к любой ссылке, где не указан протокол, необходимо добавить по умолчанию протокол httр://.
Исходный текст иногда может содержать различные символы, нарушающие xaml разметку, поэтому его необходимо HTML-кодировать перед помещением в тег Run с помощью метода WebUtility.HtmlEncode.
После всего этого формируется новая строка, состоящая из тега Paragraph с соответствующими параметрами и содержащего в себе сформированный ранее набор тегов. Xaml разметка готова.
var words = source.Split(' ');
var sbInsideTags = new StringBuilder();
sbInsideTags.Append(@"<Run Text=""");
foreach (var word in words)
{
if (Regex.IsMatch(word, @"^((https?:\/\/)?(ftps?:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,6}(:[0-9]{1,5})?(\/\S*)?)"))
{
var link = word;
link = link.Replace("&", "&");
link = Uri.EscapeUriString(link);
sbInsideTags.Append(@"""/> <Hyperlink NavigateUri=""");
sbInsideTags.Append(link.Contains("://") ? link : "http://" + link);
sbInsideTags.Append(@""">");
sbInsideTags.Append(link);
sbInsideTags.Append(@"</Hyperlink> <Run Text=""");
}
else
{
sbInsideTags.Append(WebUtility.HtmlEncode(word));
sbInsideTags.Append(' ');
}
}
var sbXaml = new StringBuilder();
sbXaml.Append(@"<Paragraph xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" TextAlignment=""Left"" FontSize=""20"" FontWeight=""Normal"" FontStyle=""Normal"" FontStretch=""Normal"" >");
sbXaml.Append(sbInsideTags);
sbXaml.Append(@" ""/></Paragraph>");
return sbXaml.ToString();