Пример приложения Message Queuing
90C# и .NET --- Сетевое программирование --- Пример приложения Message Queuing
Чтобы продемонстрировать использование Message Queuing, в этом разделе мы создадим простое приложение заказа курсов. Этот пример приложения состоит из трех сборок:
библиотека компонентов (CourseOrder), которая включает сущностные классы для сообщений, отправляемых и принимаемых в очереди;
WPF-приложение (CourseOrderSender), отправляющее сообщения в очередь;
WPF-приложение (CourseOrderReceiver), принимающее сообщения из очереди.
И отправляющее, и принимающее приложения нуждаются в информации о заказе. По этой причине сущностные классы помещаются в отдельную сборку. Сборка 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();
}
...
Главный метод задачи — 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);
}
На рисунке показано работающее приложение для приема заказов, в котором отображены три заказа из очереди, из которых один в данный момент выбран: