RAD и Monolithic

169

В этом разделе мы рассмотрим историю графических (или GUI) паттернов. Графические шаблоны проектирования разрабатываются более 30 лет и полностью описать все эти паттерны не представляется возможным. Вместо этого опишем две основные тенденции, которые появились за последние 30 лет и посмотрим, как эти две тенденции в конечном итоге превратилась в паттерн MVVM для Silverlight и WPF.

Если вы заинтересованы в получении дополнительной информации об истории графических паттернов, то обязательно прочитайте статью Мартина Фаулера - GUI Architectures.

Monolithic design

Корпоративные приложения используются для отображения, обработки и сохранения данных. Если мы строим приложение без использования паттернов проектирования, то можем столкнуться с потенциальной проблемой при расширении графического интерфейса приложения или дополнении кода доступа к данным. На следующей диаграмме показана тенденция "монолитного" проектирования графических интерфейсов:

Monolithic design

Как видите бизнес-логика и код доступа к данным инкапсулируются глубоко в графическом интерфейсе. Чтобы понять эту модель рассмотрим пример построения приложения, использующего монолитную модель проектирования.

Пример monolithic design

Итак, для начала создадим код доступа к данным, инкапсулировав его в класс ProjectBilling.DataAccess. Создайте новое решение в Visual Studio и добавьте данный класс:

Создание кода доступа к данным в monolithic design

Теперь удалите файл Class1.cs, который создается по умолчанию шаблоном проекта и добавьте новый класс Project.cs со следующим кодом:

namespace ProjectBilling.DataAccess
{
    public interface IProject
    {
        int ID { get; set; }
        string Name { get; set; }
        double Estimate { get; set; }
        double Actual { get; set; }
        void Update(IProject project);
    }

    public class Project : IProject
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public double Estimate { get; set; }
        public double Actual { get; set; }

        public void Update(IProject project)
        {
            Name = project.Name;
            Estimate = project.Estimate;
            Actual = project.Actual;
        }
    }
}

Есть, конечно, лучшие варианты, чем с помощью интерфейса обновлять метод, для обеспечения обновления объектов данных, но этот подход позволит нам использовать код для моделей Monolith и RAD (рассматривается позже).

Класс Project представляет собой простой бизнес-объект, который хранит имя проекта, ID, сметную и фактическую стоимости. Он реализован с интерфейсом, чтобы обеспечить большую гибкость, и это обеспечивает обновление значений экземпляра объекта.

Теперь мы создадим службу данных, которая будет возвращать данные для различных клиентов, при этом мы не должны быть обеспокоены кодом доступа к данным, а вместо этого сосредоточимся на представлении модели. Добавьте в проект файл DataService.cs со следующим кодом:

using System.Collections.Generic;

namespace ProjectBilling.DataAccess
{
    public interface IDataService
    {
        IList<Project> GetProjects();
    }

    public class DataServiceStub : IDataService
    {
        public IList<Project> GetProjects()
        {
            List<Project> projects = new List<Project>()
            {
                new Project()
                {
                    ID = 0,
                    Name = "Halloway",
                    Estimate = 500
                },
                new Project()
                {
                    ID = 1,
                    Name = "Jones",
                    Estimate = 1500
                },
                new Project()
                {
                    ID = 2,
                    Name = "Smith",
                    Estimate = 2000
                }
            };

            return projects;
        }
    }
}

Этот класс предоставляет один метод, называемый GetProjects(), который создает три проекта, а затем возвращает их в виде IList<Project>. Мы реализовали нашу службу данных на основе интерфейса для поддержки внедрения зависимостей (dependency injection).

Внедрение зависимостей - это шаблон, в котором зависимость создается внутри внешнего компонента, чуть позже вы увидите как это работает.

Теперь реализуем наш проект доступа к данным в виде WPF-приложения, используя "монолитный" стиль. Сначала добавим консольное приложение в наше решение:

Создание графического интерфейса в monolithic design

Мы будем преобразовывать это приложение так, чтобы можно было запустить WPF приложение из консольного приложения. Итак, добавьте ссылки на сборки PresentationFramework, PresentationCore, Xaml и WindowsBase, а также на ProjectBilling.DataAccess, созданную ранее, как показано на следующем скриншоте:

Добавление ссылок на сборки

Удалите из созданого проекта файл Program.cs и добавьте файл ProjectsView.cs со следующим содержимым:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using ProjectBilling.DataAccess;
using System.Windows.Media;

namespace ProjectBilling.UI.Monolithic
{
    sealed class ProjectsView : Window
    {

    }
}

Сейчас этот класс является производным от System.Windows.Window, что и позволяет ему отображаться в виде приложения WPF. Добавьте метод Main():

[STAThread]
static void Main(string[] args)
{
     ProjectsView mainWindow = new ProjectsView();
     new Application().Run(mainWindow);
}

Метод Main() снабжен атрибутом STAThread - что указывает на однопоточную работу этого метода - одно из требований для проектов WPF и взаимодействия с COM (Component Object Model). Здесь создается объект ProjectsView и затем он передается в метод System.Windows.Application.Run(), который инициализирует приложение WPF, начинается цикл обработки сообщений, а затем отображаетcя ProjectsView в новом окне.

Большая часть работы приложения будет осуществляться в конструкторе ProjectsView. Добавьте следующие поля в классе:

private static readonly Thickness _margin = new Thickness(5);
private readonly ComboBox _projectsComboBox = new ComboBox() { Margin = _margin };
private readonly TextBox _estimateTextBox = new TextBox() { IsEnabled = false, Margin = _margin };
private readonly TextBox _actualTextBox = new TextBox() { IsEnabled = false, Margin = _margin };

private readonly Button _updateButton = new Button()
{
        IsEnabled = false,
        Content = "Update",
        Margin = _margin
};

Здесь мы создали элемент ComboBox (перечисляет все проекты), два элемента TextBox (сметная и фактическая стоимость проекта) и кнопку для обновления.

Далее создадим конструктор для класса ProjectView, зададим некоторые свойства для изменения вида окна (его заголовок и размеры), вызовем два метода LoadProjects() и AddControlsToWindow(), реализация которых показана ниже, и добавим обработчик события клика по кнопке Update:

public ProjectsView()
{
            Title = "Project";
            Width = 250;
            MinWidth = 250;
            Height = 180;
            MinHeight = 180;

            LoadProjects();
            AddControlsToWindow();

            _updateButton.Click += updateButton_Click;
}

Добавьте следующие обработчики событий:

Теперь добавим вспомогательные методы:

        // Добавляем коллекцию проектов в ComboBox, используя класс доступа к данным
        // Привязываем видимую часть списка к свойству Name
        // Добавляем обработчик событий SelectionChangedEventHandler
        private void LoadProjects()
        {
            foreach (Project project in new DataServiceStub().GetProjects())
            {
                _projectsComboBox.Items.Add(project);
            }

            _projectsComboBox.DisplayMemberPath = "Name";
            _projectsComboBox.SelectionChanged += new SelectionChangedEventHandler(
                projectsListBox_SelectionChanged);
        }

        // Добавляем в окно элемент UniformGrid
        // и размещаем в нем дочерние элементы
        private void AddControlsToWindow()
        {
            UniformGrid grid = new UniformGrid() { Columns = 2 };

            grid.Children.Add(new Label() { Content = "Проект:" });
            grid.Children.Add(_projectsComboBox);
            
            Label label = new Label() { Content = "Сметная стоимость:" };
            grid.Children.Add(label);
            grid.Children.Add(_estimateTextBox);
            
            label = new Label() { Content = "Фактическая стоимость:"};
            grid.Children.Add(label);
            grid.Children.Add(_actualTextBox);
            grid.Children.Add(_updateButton);

            Content = grid;
        }

        // Создает таблицу размером 2x3
        private Grid GetGrid()
        {
            Grid grid = new Grid();

            grid.ColumnDefinitions.Add(new ColumnDefinition());
            grid.ColumnDefinitions.Add(new ColumnDefinition());
            grid.RowDefinitions.Add(new RowDefinition());
            grid.RowDefinitions.Add(new RowDefinition());
            grid.RowDefinitions.Add(new RowDefinition());
            grid.RowDefinitions.Add(new RowDefinition());

            return grid;
        }

        // При выборе элемента в ComboBox вызывается этот метод
        // Сначала извлекаем выбранный элемент, преобразуя его к классу Project,
        // затем активируем текстовые поля и указываем в них значения
        // Вызываем SetEstimateColor() для сравнения сметной и фактической стоимостей
        private void UpdateDetails()
        {
            Project selectedProject = _projectsComboBox.SelectedItem as Project;

            _estimateTextBox.IsEnabled = true;
            _estimateTextBox.Text = selectedProject.Estimate.ToString();
            _actualTextBox.IsEnabled = true;
            _actualTextBox.Text = (selectedProject.Actual == 0) ? "" : selectedProject.Actual.ToString();
            SetEstimateColor(selectedProject);
            _updateButton.IsEnabled = true;
        }

        // Если в ComboBox ничего не выбрано отключаем доступ к элементам управления
        private void DisableDetails()
        {
            _estimateTextBox.IsEnabled = false;
            _actualTextBox.IsEnabled = false;
            _updateButton.IsEnabled = false;
        }

        // Подсвечиваем текстовые поля в зависимости от сметной и фактической стоимостей
        private void SetEstimateColor(Project selectedProject)
        {
            if (selectedProject.Actual == 0)
            {
                this._estimateTextBox.Foreground = _actualTextBox.Foreground;
            }
            else if (selectedProject.Actual <= selectedProject.Estimate)
            {
                this._estimateTextBox.Foreground = Brushes.Green;
            }
            else
            {
                this._estimateTextBox.Foreground = Brushes.Red;
            }
        }

Запустите этот проект. Вы получите примерно следующий результат:

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

RAD

Microsoft приложила много усилий в области развития и создания модели Rapid Application Development (RAD), которая использует инструменты, позволяющие разработчикам использовать возможности таких IDE-сред, как Visual Studio. Примером использования быстрой разработки можно считать простое перетаскивание элементов управления на поверхность разработки IDE, а затем настройка этих элементов управления с помощью дизайнера IDE. Затем создается "монолитная" бизнес-логика для работы приложения.

Давайте рассмотрим создание WPF-приложения с использованием RAD. Добавьте проект WPF Application в Visual Studio и добавьте ссылку на проект ProjectBilling.DataAccess. Откройте MainWindow.xaml, а также панели Toolbox и DataSources (View --> Other Windows).

Первым шагом является добавление объекта источника данных для подключения к DataService. В окне DataSources щелкните на кнопке Add New Data Source:

Добавить новый источник данных

Далее, в появившемся диалоговом окне вы должны указать источник данных. В нашем случае используется объект:

Вам будет предоставлена ​​возможность выбрать объект, который будет источником данных. Выберите объект Project, как показано на следующем скриншоте:

Далее, выберите ComboBox для свойства Name из выпадающего меню, как показано на следующем рисунке:

Привязка источника данных и элемента управления

Теперь перетащите столбец Name на поверхность проектирования, при этом Visual Studio сгенерирует код, чтобы создать ComboBox, который возьмет на себя обязательства по отображению IList<Product>. Посмотрите на сгенерированную XAML-разметку окна:

<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
        xmlns:DataAccess="clr-namespace:ProjectBilling.DataAccess;assembly=ProjectBilling.DataAccess" 
        mc:Ignorable="d" x:Class="ProjectBilling.RAD.MainWindow"
        Title="MainWindow" Height="250" Width="350" Loaded="Window_Loaded_1">
    <Window.Resources>
        <CollectionViewSource x:Key="projectViewSource" 
                              d:DesignSource="{d:DesignInstance {x:Type DataAccess:Project}, CreateList=True}"/>
    </Window.Resources>
    <Grid>
        <Grid x:Name="grid1" DataContext="{StaticResource projectViewSource}" HorizontalAlignment="Left" 
              Margin="17,15,0,0" VerticalAlignment="Top">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <Label Content="Name:" Grid.Column="0" HorizontalAlignment="Left" Margin="3" Grid.Row="0" VerticalAlignment="Center"/>
            <ComboBox x:Name="nameComboBox" Grid.Column="1" DisplayMemberPath="Name" HorizontalAlignment="Left" 
                      Height="Auto" ItemsSource="{Binding}" Margin="3" Grid.Row="0" VerticalAlignment="Center" Width="120">
                <ComboBox.ItemsPanel>
                    <ItemsPanelTemplate>
                        <VirtualizingStackPanel/>
                    </ItemsPanelTemplate>
                </ComboBox.ItemsPanel>
            </ComboBox>
        </Grid>

    </Grid>
</Window>

Обратите внимание, что в сгенерированной разметке во-первых добавлен обработчик события загрузки окна Window_Loaded_1, во-вторых коллекция проектов определяется в виде ресурса projectViewSource, в-третьих в корневом элементе Grid создается контекст данных используя свойство DataContext, ну и в-четвертых элемент ComboBox использует данный контекст для привязки к коллекции IList<Product>. Так, за несколько секунд мы построили графический интерфейс, реализующий привязку к коллекции данных, более того, очевидным преимуществом использования данной модели является инкапсуляция логики графического интерфейса в XAML-разметке, в коде окна добавлен лишь один обработчик события загрузки окна, инициализирующий коллекцию projectViewSource в коде. Добавьте вызов метода DataServiceStub.GetProjects():

private void Window_Loaded_1(object sender, RoutedEventArgs e)
{
            System.Windows.Data.CollectionViewSource projectViewSource = 
                ((System.Windows.Data.CollectionViewSource)(this.FindResource("projectViewSource")));
            projectViewSource.Source = new ProjectBilling.DataAccess.DataServiceStub().GetProjects();
}

Данное приложение пока уступает функциональности рассмотренного при реализации монолитной модели, но используя возможности Visual Studio вы без проблем доведете его до этого вида.

Глядя на код, который мы только что создали, мы видим, что ситуация немного лучше, чем с монолитной моделью. Использование CollectionViewSource сокращает небольшое количество кода, который был создан в монолитной модели. Но самым главным преимуществом RAD-разработки, конечно, является легкость разработки. Весь код графического интерфейса и его привязки к данным генерируется IDE-средой и нам не нужно беспокоиться о деталях его реализации.

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