← Zurück

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

Можно было бы уже остановиться на достигнутом. Но бородатые дяди в очках все время придумывают какие-то серебрянные пули. Видите ли, уважаемый программист не будет писать кучу кода в форме, а будет использовать паттерн 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#. Введение”.

War dieses Thema hilfreich?

Thema teilen

Kommentare (10)

Anmelden oder Konto erstellen, Melden Sie sich an oder registrieren Sie sich, um einen Kommentar zu hinterlassen

AlexLan73
AlexLan73

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

ra81
ra81

AlexLan73: Замечательные примеры. Большое Вам спасибо. А Вы можете сделать пример - вывод индикаторов RSI, MACD ... в новом окне? Я сделал несколько иначе. Сама стратегия оформляется в виде библиотеки. Библиотека подключается в общую систему, в ней же лежит визуализацию самой стратегии. В том числе и кнопка и график.

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

VassilSanych
VassilSanych

ra81: Сама стратегия оформляется в виде библиотеки. Кстати. Никто не пробовал csscript?

AlexLan73
AlexLan73

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

Дюшес
VassilSanych
VassilSanych

Где, где у нас ```csharp new MainViewModel()

В ```csharp
MainWindow : Window
```?
WinForms Hell detected :)
MVVM и рядом не стояло. Даже на MVP не тянет :)
VassilSanych
VassilSanych

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

ra81
ra81

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

anothar
anothar

По моему скромному мнению, накручивать ViewModel в WPF большого смысла нет. Вот пример ответа на stackoverflow.com:wpf mvvm. Любой паттерн служит определенной цели и модели построения приложений. Нужно его применять там, где он будет востребован и приносить пользу. А цель программирования, как известно, - борьба со сложностью. Сложности бывают как известно разные. То, что хорошо при написании калькулятора, может не подойти при написании реального приложения. Хорошо бы еще раскрыть в статье использование каких-то известных фреймворков типа Prism, Caliburn и т.д.

VassilSanych
VassilSanych

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

В "реальном приложении" обычно бывает так:

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

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

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

VassilSanych
VassilSanych

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