понедельник, 7 ноября 2011 г.

MessageBox + FlowDocument

После того как я познакомился с магией технологии WPF и, в частности, с возможностями отображения потокового содержимого, меня очень удивило отсутствие штатной возможности вывести пользователю простое сообщение с использованием FlowDocument. И, в некоторый момент, я в своем проекте реализовал простейший механизм для отображения таких сообщений.

imageЯ использовал FlowDocument в первую очередь из-за его возможностей по использованию гиперссылок в тексте. Мне нужно было дать возможность использовать ссылку на папку (щелчок по ссылке должен открыть папку в проводнике), а также несколько ссылок, определяющих дальнейшее действие.

Таких сообщений по ходу программы возникает много, и для каждого из них городить огород из отдельного класса Window со всеми обработчиками кликов не хотелось.

Окно сообщения

После нескольких неудачных экспериментов был получен совершенно примитивный класс FlowMessageBox, унаследованный от Window, который имеет свойство Document типа FlowDocument. Этот документ отображается в окне:

<Window x:Uid="mywindow" x:Class="ProjectsProfiler.Elements.FlowMessageBox"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:ctrl="clr-namespace:ProjectsProfiler.Controls;assembly=ProjectsProfiler.Controls"
        Title="FlowMessageBox" Closing="Window_Closing" SizeToContent="WidthAndHeight" WindowStartupLocation="CenterOwner" 
        Background="{StaticResource DialogBKG}" x:Name="mywindow" ResizeMode="NoResize" ShowInTaskbar="False" WindowStyle="ToolWindow" >
    <Grid x:Uid="Grid_1" Margin="20,10">
        <ctrl:FlowDocumentViewer x:Uid="ctrl:FlowDocumentViewer_1" Style="{StaticResource SimpleFlowDocumentViewer}" 
                                 Document="{Binding Document, ElementName=mywindow}" MaxWidth="300" MaxHeight="250">
            
        </ctrl:FlowDocumentViewer>
        
    </Grid>
</Window>

Статический метод класса конструирует его, показывает сообщение и возвращает результат:



//показать сообщение с потоковым содержимым
public static string Show(FlowDocument content, string caption, Window ownerWin)
{
    var wnd = new FlowMessageBox()
    {
        Document = content,
        Title = caption,
        Owner = ownerWin,
        IsClosable = false,
        ResultString = null
    };
 
    _msgStack.Push(wnd);
    try
    {
        wnd.ShowDialog();
    }
    finally
    {
        _msgStack.Pop();
        wnd.Document = null;
    }
 
 
    return wnd.ResultString;
}

Внимательный читатель заметит здесь стек сообщений _msgStack. О его назначении я расскажу ниже.


Документ для отображения в сообщении


При показе сообщений в виде потокового содержимого возникла сложность – кнопки вида ОК, Cancel и т.п. нельзя объявить заранее и предусмотреть коды возврата. Поэтому я решил использовать строковые коды возврата. Вторая сложность заключалась в том, что код обработки щелчка по ссылке с выбором находится не в классе окна сообщения, а в том классе, в ресурсах которого объявлен соответствующий FlowDocument. Чтобы обойти эту проблему я завел глобальный стек открытых сообщений и статический метод, позволяющий закрыть верхнее сообщение с указанным результатом (используется тот факт, что пользователю доступно для взаимодействия только верхнее сообщение, т.к. сообщение показывается модальным диалогом):



[ThreadStatic]
static Stack<FlowMessageBox> _msgStack = new Stack<FlowMessageBox>();
 
//завершить верхнее открытое сообщение
public static void CloseWith(string result)
{
    Verify.IsTrue(IsOpened());
    var msg = _msgStack.Peek();
    msg.ResultString = result;
    msg.IsClosable = true;
    msg.Close();
}
 
public static bool IsOpened()
{
    return _msgStack.Count > 0;
}

Теперь можно использовать этот метод для завершения сообщения с определенным кодом возврата в обработчике щелчка по ссылке. Для простоты использования и уменьшения объема кода я поместил требуемый код возврата в свойство Tag ссылки:



<FlowDocument x:Key="SelectedEmptyFolder" Style="{StaticResource SimpleFlowDocument}" 
              TextAlignment="Left" >
    <Paragraph >
        Папка
        <Hyperlink x:Name="link1" Click="folderLink_Click"/> не содержит файлов.
    </Paragraph>
    <Paragraph Margin="60,0,0,0">
        Вы можете:
    </Paragraph>
    <List Margin="60,0,0,0">
        <ListItem >
            <Paragraph  >
                <Hyperlink Tag="new"  
                           Click="ActionLinkClick">Создать в этой папке новое хранилище</Hyperlink>
            </Paragraph>
        </ListItem>
        <ListItem >
            <Paragraph  >
                <Hyperlink Tag="other"  
                           Click="ActionLinkClick">Выбрать другую папку</Hyperlink>
            </Paragraph>
        </ListItem>
        <ListItem >
            <Paragraph  >
                <Hyperlink Tag="cancel"  
                           Click="ActionLinkClick">Отменить открытие хранилища</Hyperlink>
            </Paragraph>
        </ListItem>
    </List>
</FlowDocument>

И в обработчике щелчка по ссылке извлекаю Tag и завершаю сообщение с нужным мне результатом:



private void ActionLinkClick(object sender, RoutedEventArgs e)
{
    var obj = sender as Hyperlink;
    FlowMessageBox.CloseWith((obj.Tag ?? string.Empty).ToString());
}

Заключение


Описанный способ дал мне возможность быстро сделать несколько различных сообщений в виде FlowDocument и использовать их для получения дополнительной информации от пользователя:



public string DisplayFlowMessage(FlowDocument messageDoc, Action<string> PerformActionByResult)
{
    var result = FlowMessageBox.Show(messageDoc, StringTable.ProductName, OwnedWindow);
    PerformActionByResult(result);
}

Очень удобной оказалась возможность хранить документы в ресурсах – это позволяет локализовывать эти сообщения без лишних хлопот.

2 комментария:

  1. Осталось только понять, зачем нужен сам MessageBox?
    Я считаю, что от него нужно избавляться - есть множество других более удобных и менее раздражающих альтернатив.

    ОтветитьУдалить
  2. Тоже мнение :-)
    MessageBox - это достаточно стандартное и, самое главное, привычное пользователю поведение. Это может быть и сообщение об ошибке, и запрос на подтверждение и т.п. Выводить его в виде модального диалога или в виде отдельной части интерфейса - это, как говорит один мой коллега, вопрос религии. Описанный здесь способ можно легко переработать для любого другого способа. Мне кажется, что главный результат здесь - разделение способа показа сообщения и его представления.

    ОтветитьУдалить