Работа с диалоговыми окнами при использовании паттерна MVVM может вызывать некоторые трудности. Например, если мы хотим по клику на кнопку или на пункт меню сохранять или открывать объекты с помощью диалоговых окон, не всегда может быть ясно, как вписать взаимодействие с диалоговыми окнами в MVVM.
Итак, для работы с диалоговыми окнами продолжим работу с проектом из прошлой темы. Вначале добавим в него новый интерфейс IDialogService, который будет определять функционал для работы с диалоговыми окнами:
public interface IDialogService { void ShowMessage(string message); // показ сообщения string FilePath { get; set; } // путь к выбранному файлу bool OpenFileDialog(); // открытие файла bool SaveFileDialog(); // сохранение файла }
Далее добавим в проект реализацию этого интерфейса в виде класса DefaultDialogService:
using Microsoft.Win32; using System.Windows; namespace MVVM { public class DefaultDialogService : IDialogService { public string FilePath { get; set; } public bool OpenFileDialog() { OpenFileDialog openFileDialog = new OpenFileDialog(); if (openFileDialog.ShowDialog() == true) { FilePath = openFileDialog.FileName; return true; } return false; } public bool SaveFileDialog() { SaveFileDialog saveFileDialog = new SaveFileDialog(); if (saveFileDialog.ShowDialog() == true) { FilePath = saveFileDialog.FileName; return true; } return false; } public void ShowMessage(string message) { MessageBox.Show(message); } } }
Для получения пути файла для открытия/сохранения данный класс использует стандартные классы OpenFileDialog
и
SaveFileDialog
, которые определены в пространстве имен Microsoft.Win32
. Кроме того, для отображения сообщения здесь используется метод
MessageBox.Show()
.
Для работы с файлами одного функционала по открытию/сохранению файла мало. Нам еще надо выполнять сами действия по считыванию информации из файла или ее сохранению в файл. Подобные действия, конечно, можно определить и в ViewModel. Однако для работы с информацией мы можем использовать различные типы файлов - бинарные файлы, xml, json и т.д. Для json в .NET мы можем использовать один функционал, для xml - другой, для текстовых файлов - третий и так далее. Поэтому в этом случае для работы с файлами определим в проекте общий интерфейс IFileService:
using System.Collections.Generic; namespace MVVM { public interface IFileService { List<Phone> Open(string filename); void Save(string filename, List<Phone> phonesList); } }
Первый метод предназначен для открытия файла. Он принимает путь к файлу и возвращает список объектов. Второй метод сохраняет данные из списка в файле по определенному пути.
В качестве типа файлов мы будем использовать файлы json. Поэтому для работы с ними добавим в проект библиотеку :
Затем добавим в проект следующий класс JsonFileService:
using System.Collections.Generic; using System.IO; using System.Runtime.Serialization.Json; namespace MVVM { public class JsonFileService : IFileService { public List<Phone> Open(string filename) { List<Phone> phones = new List<Phone>(); DataContractJsonSerializer jsonFormatter = new DataContractJsonSerializer(typeof(List<Phone>)); using (FileStream fs = new FileStream(filename, FileMode.OpenOrCreate)) { phones = jsonFormatter.ReadObject(fs) as List<Phone>; } return phones; } public void Save(string filename, List<Phone> phonesList) { DataContractJsonSerializer jsonFormatter = new DataContractJsonSerializer(typeof(List<Phone>)); using (FileStream fs = new FileStream(filename, FileMode.Create)) { jsonFormatter.WriteObject(fs, phonesList); } } } }
С помощью класса DataContractJsonSerializer здесь производится сериализация/десериализация объектов в файл json в виде набора List<Phone>.
Далее изменим код ApplicationViewModel:
using System.ComponentModel; using System.Runtime.CompilerServices; using System.Collections.ObjectModel; using System; using System.Linq; namespace MVVM { public class ApplicationViewModel : INotifyPropertyChanged { Phone selectedPhone; IFileService fileService; IDialogService dialogService; public ObservableCollection<Phone> Phones { get; set; } // команда сохранения файла private RelayCommand saveCommand; public RelayCommand SaveCommand { get { return saveCommand ?? (saveCommand = new RelayCommand(obj => { try { if (dialogService.SaveFileDialog() == true) { fileService.Save(dialogService.FilePath, Phones.ToList()); dialogService.ShowMessage("Файл сохранен"); } } catch (Exception ex) { dialogService.ShowMessage(ex.Message); } })); } } // команда открытия файла private RelayCommand openCommand; public RelayCommand OpenCommand { get { return openCommand ?? (openCommand = new RelayCommand(obj => { try { if (dialogService.OpenFileDialog() == true) { var phones = fileService.Open(dialogService.FilePath); Phones.Clear(); foreach (var p in phones) Phones.Add(p); dialogService.ShowMessage("Файл открыт"); } } catch (Exception ex) { dialogService.ShowMessage(ex.Message); } })); } } // команда добавления нового объекта private RelayCommand addCommand; public RelayCommand AddCommand { get { return addCommand ?? (addCommand = new RelayCommand(obj => { Phone phone = new Phone(); Phones.Insert(0, phone); SelectedPhone = phone; })); } } private RelayCommand removeCommand; public RelayCommand RemoveCommand { get { return removeCommand ?? (removeCommand = new RelayCommand(obj => { Phone phone = obj as Phone; if (phone != null) { Phones.Remove(phone); } }, (obj) => Phones.Count > 0)); } } private RelayCommand doubleCommand; public RelayCommand DoubleCommand { get { return doubleCommand ?? (doubleCommand = new RelayCommand(obj => { Phone phone = obj as Phone; if (phone != null) { Phone phoneCopy = new Phone { Company = phone.Company, Price = phone.Price, Title = phone.Title }; Phones.Insert(0, phoneCopy); } })); } } public Phone SelectedPhone { get { return selectedPhone; } set { selectedPhone = value; OnPropertyChanged("SelectedPhone"); } } public ApplicationViewModel(IDialogService dialogService, IFileService fileService) { this.dialogService = dialogService; this.fileService = fileService; // данные по умлолчанию Phones = new ObservableCollection<Phone> { new Phone { Title="iPhone 7", Company="Apple", Price=56000 }, new Phone {Title="Galaxy S7 Edge", Company="Samsung", Price =60000 }, new Phone {Title="Elite x3", Company="HP", Price=56000 }, new Phone {Title="Mi5S", Company="Xiaomi", Price=35000 } }; } public event PropertyChangedEventHandler PropertyChanged; public void OnPropertyChanged([CallerMemberName]string prop = "") { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(prop)); } } }
Для работы с файлами в конструктор ApplicationViewModel передаются объекты IDialogService и IFileService:
public ApplicationViewModel(IDialogService dialogService, IFileService fileService) { this.dialogService = dialogService; this.fileService = fileService; //............ }
Затем эти объекты используются в командах OpenCommand и SaveCommand.
Также изменим код MainWindow.xaml:
<Window x:Class="MVVM.MainWindow" 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:local="clr-namespace:MVVM" xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" mc:Ignorable="d" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <Style TargetType="TextBlock"> <Setter Property="FontSize" Value="14" /> </Style> <Style TargetType="TextBox"> <Setter Property="FontSize" Value="14" /> </Style> <Style TargetType="Button"> <Setter Property="Width" Value="40" /> <Setter Property="Margin" Value="5" /> </Style> </Window.Resources> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="0.8*" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="25" /> <RowDefinition Height="*" /> <RowDefinition Height="0.2*" /> </Grid.RowDefinitions> <Menu Grid.ColumnSpan="2" > <MenuItem Header="Файл"> <MenuItem Header="Открыть" Command="{Binding OpenCommand}" /> <MenuItem Header="Сохранить" Command="{Binding SaveCommand}" /> </MenuItem> </Menu> <ListBox Grid.Row="1" ItemsSource="{Binding Phones}" SelectedItem="{Binding SelectedPhone}"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Margin="5"> <TextBlock FontSize="18" Text="{Binding Path=Title}" /> <TextBlock Text="{Binding Path=Company}" /> <TextBlock Text="{Binding Path=Price}" /> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <StackPanel Grid.Row="2" Orientation="Horizontal"> <Button Command="{Binding AddCommand}">+</Button> <Button Command="{Binding RemoveCommand}" CommandParameter="{Binding SelectedPhone}">-</Button> <Button Content="2x"> <i:Interaction.Triggers> <i:EventTrigger EventName="MouseDoubleClick"> <i:InvokeCommandAction Command="{Binding DoubleCommand}" CommandParameter="{Binding SelectedPhone}" /> </i:EventTrigger> </i:Interaction.Triggers> </Button> </StackPanel> <StackPanel Grid.Row="1" Grid.Column="1" DataContext="{Binding SelectedPhone}"> <TextBlock Text="Выбранный элемент" /> <TextBlock Text="Модель" /> <TextBox Text="{Binding Title, UpdateSourceTrigger=PropertyChanged}" /> <TextBlock Text="Производитель" /> <TextBox Text="{Binding Company, UpdateSourceTrigger=PropertyChanged}" /> <TextBlock Text="Цена" /> <TextBox Text="{Binding Price, UpdateSourceTrigger=PropertyChanged}" /> </StackPanel> </Grid> </Window>
В отличие от предыдущей темы здесь добавлено меню с двумя пунктами, которые привязаны к командам SaveCommand и OpenCommand:
<Menu Grid.ColumnSpan="2" > <MenuItem Header="Файл"> <MenuItem Header="Открыть" Command="{Binding OpenCommand}" /> <MenuItem Header="Сохранить" Command="{Binding SaveCommand}" /> </MenuItem> </Menu>
И в конце изменим файл связанного кода MainWindow.xaml.cs:
using System.Windows; namespace MVVM { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext = new ApplicationViewModel(new DefaultDialogService(), new JsonFileService()); } } }
В конструктор ApplicationViewModel для работы с файлами передаются объекты DefaultDialogService и JsonFileService.