Создание роботов с помощью S#. Часть 5. Использование паттерна MVVM

Создание роботов с помощью S#. Часть 5. Использование паттерна MVVM
Atom
19.03.2012
FinDirector


Можно было бы уже остановиться на достигнутом. Но бородатые дяди в очках все время придумывают какие-то серебрянные пули. Видите ли, уважаемый программист не будет писать кучу кода в форме, а будет использовать паттерн MVVM (Model-View-ViewModel). Не существует какого-то единственно правильного способа реализовать этот паттерн, поэтому предлагаю самый простейший вариант.
Нам понадобится интерфейс IVIew и базовый класс для ViewModel:

Код
	public interface IView
	{
		object DataContext { get; set; }
	}

	public abstract class ViewModel : INotifyPropertyChanged
	{
		public IView View { get; protected set; }

		protected ViewModel() { }

		#region INotifyPropertyChanged Members

		public event PropertyChangedEventHandler PropertyChanged;

		protected void RaisePropertyChanged(string propertyName)
		{
			PropertyChangedEventHandler handler = PropertyChanged;
			if (handler == null) return;
			handler(this, new PropertyChangedEventArgs(propertyName));
		}

		#endregion
	}


В нашем простом приложении будет две View и две ViewModel.
StrategyViewModel.cs

Код
	class StrategyViewModel : ViewModel
	{
		public StandardStrategy Strategy { get; private set; }
		public DelegateCommand StartCommand { get; private set; }
		public DelegateCommand StopCommand { get; private set; }
		public DelegateCommand RunTerminalCommand { get; private set; }

		public StrategyViewModel(StandardStrategy strategy)
		{
			this.Strategy = strategy;

			this.StartCommand = new DelegateCommand(OnStartExecuted, CanStart);
			this.StopCommand = new DelegateCommand(OnStopExecuted, CanStop);
			this.RunTerminalCommand = new DelegateCommand(RunTerminal);
			this.Strategy.ProcessStateChanged += s =>
			{
				this.StartCommand.RaiseCanExecuteChanged();
				this.StopCommand.RaiseCanExecuteChanged();
			};

			this.View = new StrategyView();
			this.View.DataContext = this;
		}

		public bool CanStart()
		{
			return Strategy.ProcessState == ProcessStates.Stopped;
		}

		public void OnStartExecuted()
		{
			if (!CanStart())
				return;
			Task.Factory.StartNew(Strategy.Start);
		}

		public bool CanStop()
		{
			return Strategy.ProcessState == ProcessStates.Started;
		}

		public void OnStopExecuted()
		{
			if (!CanStop())
				return;
			MessageBoxResult result = MessageBox.Show(
				String.Format("Вы уверены, что хотите остановить стратегию {0}?", Strategy.Name),
				"Подтверждение", MessageBoxButton.OKCancel);
			if (result == MessageBoxResult.OK)
				Strategy.Stop();
		}

		public void RunTerminal()
		{
			MessageBoxResult result = MessageBox.Show(
				String.Format("Вы уверены, что хотите запустить терминал {0}?", Strategy.TraderBuilder.Title),
				"Подтверждение", MessageBoxButton.OKCancel);
			if (result == MessageBoxResult.OK)
				Strategy.TraderBuilder.RunTerminal();
		}
	}


StrategyView помечаем интерфейсом IView:

StrategyView.xaml.cs
Код
	public partial class StrategyView : UserControl, IView
	{
		public StrategyView()
		{
			InitializeComponent();
		}
	}


StrategyView.xaml
Код
    <UserControl.Resources>
        <Style x:Key="stValue" TargetType="FrameworkElement">
            <Setter Property="HorizontalAlignment" Value="Right" />
        </Style>
    </UserControl.Resources>
    <StackPanel Margin="3">
        <StackPanel Margin="5">
            <TextBlock Text="{Binding Strategy.Name}" FontSize="14" />
            <TextBlock Text="{Binding Strategy.Description}" FontSize="9"  />
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition />
                    <ColumnDefinition />
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                </Grid.RowDefinitions>
                <TextBlock Text="Терминал: " Grid.Row="0" Grid.Column="0" />
                <Button Grid.Row="0" Grid.Column="1"
                                        HorizontalAlignment="Right"
                                        Content="{Binding Strategy.TraderBuilder.Title}"
                                        Command="{Binding Path=RunTerminalCommand}" />

                <TextBlock Text="Портфель: " Grid.Row="1" Grid.Column="0" />
                <TextBlock Text="{Binding Strategy.PortfolioSelector.Title}" Grid.Row="1" Grid.Column="1" Style="{StaticResource stValue}"/>

                <TextBlock Text="Инструмент: " Grid.Row="2" Grid.Column="0" />
                <TextBlock Text="{Binding Strategy.SecuritySelector.Title}" Grid.Row="2" Grid.Column="1" Style="{StaticResource stValue}"/>

                <TextBlock Text="Состояние: " Grid.Row="3" Grid.Column="0" />
                <TextBlock Text="{Binding Strategy.ProcessState}" Grid.Row="3" Grid.Column="1" Style="{StaticResource stValue}"/>

                <TextBlock Text="Прибыль: " Grid.Row="4" Grid.Column="0"/>
                <TextBlock Text="{Binding Strategy.PnLManager.PnL}" Grid.Row="4" Grid.Column="1" Style="{StaticResource stValue}"/>

                <TextBlock Text="Позиция: " Grid.Row="5" Grid.Column="0"/>
                <TextBlock Text="{Binding Strategy.PositionManager.Position}" Grid.Row="5" Grid.Column="1" Style="{StaticResource stValue}"/>
            </Grid>
            <StackPanel Orientation="Horizontal" Margin="0,5,0,0" HorizontalAlignment="Center">
                <Button x:Name="btnStartStrategy" Content="Старт" Command="{Binding Path=StartCommand}" Margin="2,0,0,0" />
                <Button x:Name="btnStopStrategy" Content="Стоп" Command="{Binding Path=StopCommand}" Margin="2,0,0,0" />
            </StackPanel>
        </StackPanel>
    </StackPanel>


MainViewModel.cs
Код
	class MainViewModel : IDisposable
	{
		public ObservableCollection<StrategyViewModel> Strategies { get; private set; }
		public ILogListener LogView { get; private set; }
		private LogManager LogManager;

		public MainViewModel()
		{
			Strategies = new ObservableCollection<StrategyViewModel>();
			CreateLoggers();
		}

		private void CreateLoggers()
		{
			LogManager = new LogManager();

			LogView = new StockSharp.Xaml.Monitor();
			LogManager.Listeners.Add(LogView);

			var fileListener = new FileLogListener(
				string.Format(CultureInfo.InvariantCulture, "{0:yyyyMMdd}.txt", DateTime.Today));
			LogManager.Listeners.Add(fileListener);

			if (!string.IsNullOrEmpty(Settings.GoogleLogin))
			{
				var smsListener = new SmsLogListener(Settings.GoogleLogin, Settings.GooglePassword);
				smsListener.Filters.Add(LogListener.AllErrorFilter);
				LogManager.Listeners.Add(smsListener);
			}
		}

		public void Dispose()
		{
			if (LogManager != null)
				LogManager.Dispose();
			if (Strategies != null)
			{
				foreach (ITraderBuilder traderBuilder in Strategies.Select(s => s.Strategy.TraderBuilder).Distinct())
				{
					if (traderBuilder != null)
						traderBuilder.Dispose();
				}
			}
		}

		public void LoadStrategies()
		{
			StrategyLoader strategyLoader = StrategyLoader.Load("Strategies.config");
			Strategies.Clear();
			foreach (StandardStrategy strategy in strategyLoader.Strategies)
			{
				LogManager.Sources.Add(strategy);
				StrategyViewModel strategyModel = new StrategyViewModel(strategy);
				Strategies.Add(strategyModel);
			}
			foreach (ILogSource logSource in strategyLoader.Strategies
				.Select(s => s.TraderBuilder).OfType<ILogSource>().Distinct())
			{
				LogManager.Sources.Add(logSource);
			}
		}
	}


MainWindow.xaml.cs

Код
	public partial class MainWindow : Window
	{
		private MainViewModel viewModel;

		public MainWindow()
		{
			InitializeComponent();
			this.Loaded += Window_Loaded;

			this.viewModel = new MainViewModel();
			this.DataContext = viewModel;
		}

		private void Window_Loaded(object sender, RoutedEventArgs e)
		{
			viewModel.LoadStrategies();
		}

		protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
		{
			base.OnClosing(e);
			MessageBoxResult result = MessageBox.Show(
				"Вы уверены, что хотите завершить работу?",
				"Подтверждение", MessageBoxButton.OKCancel);
			if (result != MessageBoxResult.OK)
				e.Cancel = true;
		}

		protected override void OnClosed(EventArgs e)
		{
			base.OnClosed(e);
			if (viewModel != null)
				viewModel.Dispose();
		}
	}


MainWindow.xaml
Код
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto">
            <ItemsControl ItemsSource="{Binding Path=Strategies}">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <WrapPanel ></WrapPanel>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <ContentControl Content="{Binding View}" />
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </ScrollViewer>

        <ContentControl Grid.Row="1" Content="{Binding LogView}" />
    </Grid>


Остается добавить к приложению стили и оно станет выглядеть как в статье “Создание роботов с помощью S#. Введение”.


AlexLan73

Фотография
Дата: 29.06.2012
Ответить


Замечательные примеры.
Большое Вам спасибо.
А Вы можете сделать пример - вывод индикаторов RSI, MACD ... в новом окне?
Спасибо:

ra81

Фотография
Дата: 30.06.2012
Ответить


AlexLan73
Замечательные примеры.
Большое Вам спасибо.
А Вы можете сделать пример - вывод индикаторов RSI, MACD ... в новом окне?

Я сделал несколько иначе. Сама стратегия оформляется в виде библиотеки. Библиотека подключается в общую систему, в ней же лежит визуализацию самой стратегии. В том числе и кнопка и график.

По сути система это коробка, куда пихаются кубики.
Спасибо:

AlexLan73

Фотография
Дата: 30.06.2012
Ответить


Спасибо за ответ.
Все просто и наглядно.
Спасибо: Дюшес

VassilSanych

Фотография
Дата: 13.07.2012
Ответить


Где, где у нас
Код
new MainViewModel()
?
В
Код
MainWindow : Window
?
WinForms Hell detected :)
MVVM и рядом не стояло. Даже на MVP не тянет :)
Спасибо:

VassilSanych

Фотография
Дата: 13.07.2012
Ответить


Но вообще-то это была шутка (хотя в контексте статьи шуткой и не является).
Сейчас объясню почему.
По моему скромному мнению, накручивать ViewModel в WPF большого смысла нет. Потому что WPF и так организован по принципу MVVM.
Где behind code является фактически этим самым ViewModel (в отличии от форм WinForms), потому что он уже слабо отвечает за отображение формы (должен слабо отвечать), а больше отвечает за её поведение. А в процессе отображения уже участвует не только behind code, но и куча сервисов собственно WPF.
В общем в контексте WPF: view - это XAML, viewModel - это behind code, а model - это собственно модель.
Накручивать свои архитектурные нахлобучки на WPF конечно можно ("Любую проблему можно решить введением дополнительного уровня абстракции, кроме проблемы слишком большого количества уровней абстракции" :) )
Но, как показывает практика, это обычно усложняет систему и увеличивает трудоёмкость (даже в очень крупных системах). А цель программирования, как известно, - борьба со сложностью.
;)
Спасибо:

VassilSanych

Фотография
Дата: 13.07.2012
Ответить


ra81
Сама стратегия оформляется в виде библиотеки.

Кстати.
Никто не пробовал csscript?

Спасибо:

ra81

Фотография
Дата: 13.07.2012
Ответить


VassilSanych

По моему скромному мнению, накручивать ViewModel в WPF большого смысла нет. Потому что WPF и так организован по принципу MVVM.

Если системы делаются в виде отдельных библиотек включающих код и отображение, без паттерна не обойтись. Интеграция библиотеки в общую оболочку может встать дорого :). Я тоже реализовал себе сей паттерн. Меня устраиват полностью.
Спасибо:

anothar

Фотография
Дата: 14.07.2012
Ответить


Цитата:
По моему скромному мнению, накручивать ViewModel в WPF большого смысла нет.

Вот пример ответа на stackoverflow.com:wpf mvvm. Любой паттерн служит определенной цели и модели построения приложений. Нужно его применять там, где он будет востребован и приносить пользу.
Цитата:
А цель программирования, как известно, - борьба со сложностью.

Сложности бывают как известно разные. То, что хорошо при написании калькулятора, может не подойти при написании реального приложения.
Хорошо бы еще раскрыть в статье использование каких-то известных фреймворков типа Prism, Caliburn и т.д.
Спасибо:

VassilSanych

Фотография
Дата: 19.07.2012
Ответить


anothar
Цитата:
По моему скромному мнению, накручивать ViewModel в WPF большого смысла нет.

То, что хорошо при написании калькулятора, может не подойти при написании реального приложения.

В "реальном приложении" обычно бывает так:
- архитектор/ведущий разработчик/etc накрутил 4 звена, в каждом по 5 "правильных" слоёв (действительно правильных со стройной архитектурой),
- документацию по архитектуре ему писать либо лень, либо "потом напишу", либо она просто устаревает намного быстрее полёта его мысли,
- в команде 10-20 разработчиков,
- все более-менее в курсе зон ответственности звеньев (архитектор за этим старается следить, да VS не даст использовать зависимости чужого звена), но не особо разбираются в идеях зон ответственности слоёв
- соответственно при кодировании зоны ответственности слоёв нарушаются, в проекте хренова туча кода - за каждым не уследишь (при том, что всё замечательно работает и проходит все тесты)
- на выходе имеем жесточайший бардак в проекте -> проблемы с рефакторингом -> медленная и мучительная смерть проекта

"Сделай настолько просто, насколько это возможно, но не проще." Эйнштейн

PS
Я видел это в коде бизнес-приложения Microsoft, например. Кстати это тоже было интеграционное решение, как и StockSharp. C похожими проблемами политики качества (Для обеспечения регрессионного тестирования таких приложений нужно примерно в 3 раза больше ресурсов, чем собственно для создания приложения. Причём ресурсов как минимум такого же качества).
Спасибо:

VassilSanych

Фотография
Дата: 30.11.2012
Ответить


Автор использовал
using Microsoft.Practices.Prism.Commands;
http://compositewpf.codeplex.com/releases/view/95815
Спасибо:


Добавить файлы через драг-н-дроп, , или вставить из буфера обмена.

loading
clippy