Пример приложения Message Queuing

90

Чтобы продемонстрировать использование Message Queuing, в этом разделе мы создадим простое приложение заказа курсов. Этот пример приложения состоит из трех сборок:

И отправляющее, и принимающее приложения нуждаются в информации о заказе. По этой причине сущностные классы помещаются в отдельную сборку. Сборка CourseOrder включает три сущностных класса: CourseOrder, Course и Customer. В этом примере приложения реализованы не все свойства, которые могут присутствовать в реальном приложении, а лишь столько, сколько достаточно для демонстрации концепций.

В файле Course.cs определен класс Course. Этот класс имеет лишь одно свойство для названия курса:

// Course.cs
namespace MSMQSample
{
   public class Course
   {
      public string Title { get; set; }
   }
}

Файл Customer.cs включает класс Customer, в котором имеются свойства для компании и контактного имени:

// Customer.cs
namespace MSMQSample
{
   public class Customer
   {
      public string Company { get; set; }
      public string Contact { get; set; }
   }
}

Класс CourseOrder в файле CourseOrder.cs связывает заказчика с курсом внутри заказа и определяет приоритет заказа. Кроме того, в этом классе определено имя очереди, для которого устанавливается форматное имя общедоступной очереди. Форматное имя используется для отправки сообщения, даже если в текущий момент добраться до очереди невозможно. Получить форматное имя можно, прочитав идентификатор очереди сообщений с помощью оснастки Computer Management. Если доступ к Active Directory для создания общедоступной очереди не требуется, этот код легко изменить так, чтобы в нем применялась и частная очередь:

// CourseOrder.cs
namespace MSMQSample
{
    public class CourseOrder
    {
        public const string CourseOrderQueueName = "FormatName:Public=D99CE5F3–4282–4a97–93EE-E9558B15EB13";

        public Customer Customer { get; set; }
        public Course Course { get; set; }
    }
}

Вторая часть решения представлена приложением Windows по имени CourseOrderSender. Это приложение отправляет заказы курсов в очередь сообщений. Должны присутствовать ссылки на пространства имен System.Messaging и CourseOrder.

Пользовательский интерфейс этого приложения показан на рисунке ниже. Элементы комбинированного списка comboBoxCourses включают несколько курсов, таких как “Advanced .NET Programming”, “Programming with LINQ” и “Distributed Application Development using WCF”. В результате щелчка на кнопке Submit the Order (Отправить заказ) вызывается метод-обработчик buttonSubmit_Click(). Этот метод создает объект CourseOrder и заполняет его содержимым элементов управления TextBox и ComboBox. Затем создается экземпляр MessageQueue для открытия общедоступной очереди с форматным именем. С помощью метода Send() объект CourseOrder передается для сериализации форматировщиком по умолчанию XmlMessageFormatter и записи в очередь:

<Window x:Class="MSMQSample.CourseOrderWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Course Order" Height="300" Width="300">
    <Window.Resources>

        <Style x:Key="Style1">
            <Setter Property="Control.VerticalAlignment" Value="Center" />
            <Setter Property="Control.Margin" Value="5" />
        </Style>

        <Style TargetType="{x:Type Label}" BasedOn="{StaticResource Style1}"/>
        <Style TargetType="{x:Type Button}" BasedOn="{StaticResource Style1}" />
        <Style TargetType="{x:Type CheckBox}" BasedOn="{StaticResource Style1}" />
        <Style TargetType="{x:Type TextBox}" BasedOn="{StaticResource Style1}" />
        <Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource Style1}"></Style>

    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="1.5*" />
        </Grid.ColumnDefinitions>
        <Label x:Name="labelCourse" Grid.Row="0" Grid.Column="0">Название курса:</Label>
        <Label x:Name="labelCompany" Grid.Row="1" Grid.Column="0">Компания:</Label>
        <Label x:Name="labelContact" Grid.Row="2" Grid.Column="0">Контакт:</Label>
        <CheckBox x:Name="checkBoxPriority" Grid.Row="3" Grid.Column="0">Высокий приоритет</CheckBox>
        <ComboBox x:Name="comboBoxCourses" Grid.Row="0" Grid.Column="1">
            <ComboBoxItem>Advanced .NET Programming</ComboBoxItem>
            <ComboBoxItem>Programming with LINQ</ComboBoxItem>
            <ComboBoxItem>Distributed Applications with WCF</ComboBoxItem>
        </ComboBox>
        <TextBox x:Name="textCompany" Grid.Row="1" Grid.Column="1" />
        <TextBox x:Name="textContact" Grid.Row="2" Grid.Column="1" />
        <Button  x:Name="buttonSubmit" Click="buttonSubmit_Click" Grid.Row="3" Grid.Column="1">Отправить заказ</Button>
    </Grid>
</Window>
private void buttonSubmit_Click(object sender, RoutedEventArgs e)
{
            try
            {
                var order = new CourseOrder
                {
                    Course = new Course()
                    {
                        Title = comboBoxCourses.Text
                    },
                    Customer = new Customer
                    {
                        Company = textCompany.Text,
                        Contact = textContact.Text
                    }
                };
                
                // Для частной очереди использовать
                // using (MessageQueue queue = new MessageQueue(@".\Private$\CourseOrder"))
                using (MessageQueue queue = new MessageQueue(
                    CourseOrder.CourseOrderQueueName))
                using (var message = new Message(order))
                {
                    if (checkBoxPriority.IsChecked == true)
                    {
                        message.Priority = MessagePriority.High;
                    }
                    
                    message.Recoverable = true;
                    queue.Send(message,  String.Format("Заказ курса {{{0}}}",
                        order.Customer.Company));
                }

                MessageBox.Show("Заказ курса отправлен", "Заказ курса",
                    MessageBoxButton.OK, MessageBoxImage.Information);
            }
            catch (MessageQueueException ex)
            {
                MessageBox.Show(ex.Message, "Ошибка при создании заказа",
                    MessageBoxButton.OK, MessageBoxImage.Error);
            }
}
Приложение для отправки заказа курсов

Сообщения могут быть снабжены приоритетом за счет установки свойства Priority класса Message. Если сообщения специально сконфигурированы, то должен быть создан объект Message, и тело сообщения передано его конструктору.

В этом примере приоритет устанавливается в MessagePriority.High, если отмечен флажок checkBoxPriority. Перечисление MessagePriority позволяет устанавливать значения от Lowest(0) до Highest(7). Значение по умолчанию, Normal, соответствует величине приоритета 3. Чтобы сделать сообщение восстановимым, понадобится установить свойство Recoverable в true.

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

Добавление заказа курсов в очередь

Представление визуального конструктора приложения CourseOrderReceiver (Получатель заказов курсов), которое читает сообщения из очереди, показано на рисунке ниже. В этом приложении отображаются метки каждого заказа в списке listOrders. Когда заказ выбран, его содержимое отображается в элементах управления в правой части окна приложения.

В конструкторе класса Window по имени CourseOrderReceiverWindow создается объект MessageQueue, ссылающийся на ту же очередь, что была использована в приложении, отправляющем заказы. Для чтения сообщений форматировщик XmlMessageFormatter с читаемыми типами ассоциируется с очередью через свойство Formatter. Для отображения доступных сообщений в списке создается новая задача, которая читает сообщения в фоновом режиме. Главным методом этой задачи является PeekMessages:

<Window x:Class="MSMQSample.CourseOrderReceiverWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Course Order Receiver" Height="300" Width="400">
    <Window.Resources>
        <Style x:Key="Style1">
            <Setter Property="Control.VerticalAlignment" Value="Center" />
            <Setter Property="Control.Margin" Value="5,5,5,5" />
        </Style>
        <Style TargetType="{x:Type Label}" BasedOn="{StaticResource Style1}"/>        
        <Style TargetType="{x:Type Button}" BasedOn="{StaticResource Style1}" />
        <Style TargetType="{x:Type CheckBox}" BasedOn="{StaticResource Style1}" />
        <Style TargetType="{x:Type TextBox}" BasedOn="{StaticResource Style1}" />
        <Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource Style1}"></Style>
    </Window.Resources>
    <DockPanel>
        <Grid DockPanel.Dock = "Left">
            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
                <RowDefinition Height="4*" />
            </Grid.RowDefinitions>
            <Label x:Name="labelOrders" Grid.Row="0">Заказы</Label>
            <ListBox Grid.Row="1" x:Name="listOrders" ItemsSource="{Binding}"  SelectionChanged="listOrders_SelectionChanged"  />

        </Grid>
        <Grid DockPanel.Dock = "Right" IsEnabled="True">
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition />
                <RowDefinition />
                <RowDefinition />
                <RowDefinition />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="2*" />
            </Grid.ColumnDefinitions>
            <Label x:Name="labelCourse" Grid.Row="0" Grid.Column="0" Content="Курс:"/>
            <Label x:Name="labelCompany" Grid.Row="1" Grid.Column="0" Content="Компания:"/>
            <Label x:Name="labelContact" Grid.Row="2" Grid.Column="0" Content="Контакт:"/>
            <TextBox x:Name="textCourse" Grid.Row="0" Grid.Column="1" />
            <TextBox x:Name="textCompany" Grid.Row="1" Grid.Column="1" />
            <TextBox x:Name="textContact" Grid.Row="2" Grid.Column="1" />
            <Label x:Name="labelPriority" Grid.Row="3" Grid.Column="1" Content="ПРИОРИТЕТНЫЙ ЗАКАЗ" Visibility="Hidden"/>
            <Button x:Name="buttonProcessOrder" Grid.Row="4" Grid.Column="1" Content="Обработать заказ" IsEnabled="False" Click="buttonProcessOrder_Click"/>
        </Grid>

    </DockPanel>
</Window>
using System;
using System.Windows;
using System.Messaging;
using System.Threading.Tasks;
using System.Windows.Threading;

namespace MSMQSample
{
    public partial class CourseOrderReceiverWindow : Window
    {
        private MessageQueue orderQueue;

        public CourseOrderReceiverWindow()
        {
            InitializeComponent();

            // Для частной очереди заменить на
            // string queueName = @".\Private$\CourseOrder";
            string queueName = CourseOrder.CourseOrderQueueName;

            orderQueue = new MessageQueue(queueName);

            orderQueue.Formatter = new XmlMessageFormatter(
                new Type[]
                {
                    typeof(CourseOrder),
                    typeof(Customer),
                    typeof(Course)
                });

            // Запуск задачи, заполняющейListBox заказами
            Task t1 = new Task(PeekMessages);
            t1.Start();
        }
        
        ...
Приложение Course Order Receiver

Главный метод задачи — PeekMessages() — использует перечислитель очереди сообщений для отображения всех сообщений. Внутри цикла while перечислитель messagesEnumerator проверяет наличие нового сообщения в очереди. Если в очереди нет сообщений, организуется ожидание в течение трех часов появления нового сообщения, и работа завершается.

Поток не может напрямую писать текст в окно списка, чтобы отображать в нем каждое сообщение из очереди, а потому должен переадресовать вызов потоку, создавшему окно списка. Поскольку элементы управления WPF привязаны к одному потоку, доступ к их методам и свойствам разрешен только потоку-создателю. Метод Dispatcher.Invoke() переадресует запрос потоку-создателю:

private void PeekMessages()
{
            using (MessageEnumerator messagesEnumerator =
                  orderQueue.GetMessageEnumerator2())
            {
                while (messagesEnumerator.MoveNext(TimeSpan.FromHours(3)))
                {
                    var labelId = new LabelIdMapping()
                    {
                        Id = messagesEnumerator.Current.Id,
                        Label = messagesEnumerator.Current.Label
                    };
                    Dispatcher.Invoke(DispatcherPriority.Normal,
                          new Action<LabelIdMapping>(AddListItem), labelId);
                }
            }
            MessageBox.Show("Нет заказов за последние 3 часа. Выход из потока",
                  "Получатель сообщений о заказе", MessageBoxButton.OK,
                  MessageBoxImage.Information);
}

private void AddListItem(LabelIdMapping labelIdMapping)
{
            listOrders.Items.Add(labelIdMapping);
}

Элемент управления ListBox содержит элементы класса LabelIdMapping. Этот класс служит для отображения меток сообщений в окне списка, оставляя скрытым идентификатор каждого сообщения. Идентификатор сообщения может быть использован для последующего чтения сообщения:

private class LabelIdMapping
{
            public string Label { get; set; }
            public string Id { get; set; }

            public override string ToString()
            {
                return Label;
            }
}

Элемент управления ListBox имеет событие SelectedIndexChanged, ассоциированное с методом listOrders_SelectionChanged(). Этот метод получает объект LabelIdMapping из текущего выбора и использует идентификатор для еще одного обращения к сообщению методом PeekById(). Затем содержимое сообщения отображается в элементе управления TextBox. Поскольку по умолчанию приоритет сообщения не читается, для получения Priority должно быть установлено свойство MessageReadPropertyFilter:

private void listOrders_SelectionChanged(object sender, RoutedEventArgs e)
{
            LabelIdMapping labelId = listOrders.SelectedItem as LabelIdMapping;
            if (labelId == null)
                return;

            orderQueue.MessageReadPropertyFilter.Priority = true;
            Message message = orderQueue.PeekById(labelId.Id);

            CourseOrder order = message.Body as CourseOrder;
            if (order != null)
            {
                textCourse.Text = order.Course.Title;
                textCompany.Text = order.Customer.Company;
                textContact.Text = order.Customer.Contact;
                buttonProcessOrder.IsEnabled = true;

                if (message.Priority > MessagePriority.Normal)
                {
                    labelPriority.Visibility = Visibility.Visible;
                }
                else
                {
                    labelPriority.Visibility = Visibility.Hidden;
                }
            }
            else
            {
                MessageBox.Show("Выбранный элемент не входит в список заказов",
                      "Получатель сообщений о заказе", MessageBoxButton.OK,
                      MessageBoxImage.Warning);
            }
}

Щелчок на кнопке "Обработать заказ" приводит к вызову метода-обработчика OnProcessOrder(). Здесь опять производится обращение к текущему выбранному сообщению в окне списка, и сообщение удаляется из очереди с помощью метода ReceiveById():

private void buttonProcessOrder_Click(object sender, RoutedEventArgs e)
{
            LabelIdMapping labelId = listOrders.SelectedItem as LabelIdMapping;
            Message message = orderQueue.ReceiveById(labelId.Id);

            listOrders.Items.Remove(labelId);
            listOrders.SelectedIndex = -1;
            buttonProcessOrder.IsEnabled = false;
            textCompany.Text = string.Empty;
            textContact.Text = string.Empty;
            textCourse.Text = string.Empty;

            MessageBox.Show("Заказ курса обработан", "Получатель сообщений о заказе",
                  MessageBoxButton.OK, MessageBoxImage.Information);
}

На рисунке показано работающее приложение для приема заказов, в котором отображены три заказа из очереди, из которых один в данный момент выбран:

Приложение для приема заказов
Пройди тесты
Лучший чат для C# программистов