Решил для себя разобраться с MVVM, не совсем уверен правильно ли я организовал связь между Model и ViewModel. Хотел бы услышать ваше мнение по поводу реализации этого паттерна, а также претензии к коду.
Функционал программы на данный момент:
P.S. для удобства чтения оставлю ссылку на сам проект в комментариях.
View:
<UserControl x:Class="MinimalMVVM.Views.WindowControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ViewModels="clr-namespace:MinimalMVVM.ViewModels"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance ViewModels:WindowViewModel}" Height="475" Width="319.333">
<UserControl.InputBindings>
<KeyBinding Key="Enter" Command="{Binding ConvertTextCommand}"/>
</UserControl.InputBindings>
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="ReverseBooleanToVisibilityConverter" />
</UserControl.Resources>
<StackPanel Margin="0,22,0.333,-78">
<RadioButton Name="id_radio" IsEnabled="{Binding ElementName=http_radio}" IsChecked="True">ID</RadioButton>
<RadioButton IsChecked="{Binding IsHttp}" Name="http_radio">HTTP</RadioButton>
<GroupBox Visibility="{Binding ElementName=id_radio, Path=IsChecked, Converter={StaticResource ReverseBooleanToVisibilityConverter}}">
<StackPanel>
<Label Foreground="Blue" Margin="5,5,5,0">ID пользователя</Label>
<TextBox Text="{Binding SomeText, UpdateSourceTrigger=PropertyChanged}" Margin="5,5,5.333,5" Height="24"/>
</StackPanel>
</GroupBox>
<GroupBox Visibility="{Binding ElementName=http_radio, Path=IsChecked, Converter={StaticResource ReverseBooleanToVisibilityConverter}}">
<StackPanel>
<Label Foreground="Blue" Margin="5,5,5,0">HTTP</Label>
<TextBox Text="{Binding SomeText, UpdateSourceTrigger=PropertyChanged}" Margin="5,5,5.333,5" Height="24"/>
</StackPanel>
</GroupBox>
<Label Foreground="Blue" Margin="5,5,5,0" Height="27">History</Label>
<ListBox SelectedIndex="{Binding SelectedLineIndex}" ItemsSource="{Binding History}" Height="200" Margin="5"/>
<Button Command="{Binding ConvertTextCommand}" Margin="5" Content="Добавить строку в файл"/>
<Button Command="{Binding DeleteLineFromTextCommand}" Content="Удалить строку из файла" Margin="5,5,5.333,5"/>
<Button Command="{Binding AddTextFromFileCommand}" Content="Выбрать файл" Margin="5"></Button>
</StackPanel>
ViewModel:
namespace MinimalMVVM.ViewModels
{
public class DelegateCommand : ICommand
{
private readonly Action _action;
public DelegateCommand(Action action)
{
_action = action;
}
public void Execute(object parameter)
{
_action();
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged { add { } remove { } }
}
}
using System.ComponentModel;
namespace MinimalMVVM
{
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChangedEvent(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
WindowViewModel
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows.Input;
using MinimalMVVM.Models;
using System.IO;
using System.Linq;
using TweetSharp;
namespace MinimalMVVM.ViewModels
{
public class WindowViewModel : ObservableObject
{
#region fields
private readonly FileModel file = new FileModel();
private ObservableCollection<string> _history = new ObservableCollection<string>();
private readonly FileModel fileModel = new FileModel();
private readonly TwitterUserModel twitterUserModel = new TwitterUserModel();
private string _someText;
public string SomeText
{
get { return _someText; }
set
{
_someText = value;
RaisePropertyChangedEvent("SomeText");
}
}
private string _filePath;
public string FilePath
{
get { return _filePath; }
set
{
_filePath = value;
RaisePropertyChangedEvent("FilePath");
}
}
private int? _selectedLineIndex;
public int? SelectedLineIndex
{
get { return _selectedLineIndex; }
set
{
_selectedLineIndex = value;
RaisePropertyChangedEvent("SelectedLineIndex");
}
}
public IEnumerable<string> History
{
get { return _history; }
}
private string _userName;
public string UserName
{
get { return _userName; }
set
{
_userName = value;
RaisePropertyChangedEvent("UserName");
}
}
private long _userId;
public long UserId
{
get { return _userId; }
set
{
_userId = value;
RaisePropertyChangedEvent("UserId");
}
}
private bool _isHttp;
public bool IsHttp
{
get { return _isHttp; }
set
{
_isHttp = value;
RaisePropertyChangedEvent("IsHttp");
}
}
private static string ConsumerKey = "";
private static string ConsumerSecret = "";
private static string _accessToken = "";
private static string _accessTokenSecret = "";
private static TwitterService service = new TwitterService(ConsumerKey, ConsumerSecret, _accessToken, _accessTokenSecret);
#endregion
#region commands
public ICommand ConvertTextCommand
{
get { return new DelegateCommand(ConvertText); }
}
public ICommand AddTextFromFileCommand
{
get { return new DelegateCommand(AddTextFromFile); }
}
public ICommand DeleteLineFromTextCommand
{
get { return new DelegateCommand(DeleteLineFromText); }
}
#endregion
#region command-methods
private void ConvertText()
{
if (IsHttp == true)
{
AddHttpText();
}
else
{
AddIdText();
}
}
private void DeleteLineFromText()
{
try
{
List<string> linesList = File.ReadAllLines(file.FilePath).ToList();
linesList.RemoveAt((int)_selectedLineIndex);
File.WriteAllLines(file.FilePath, _history);
}
catch (System.ArgumentNullException) { }
catch (System.ArgumentOutOfRangeException) { }
DeleteLineHistory();
SaveChangesInFile();
}
private void AddTextFromFile()
{
ClearHistory();
SelectFile();
GetTextFromFile();
SaveChangesInFile();
}
#endregion
#region helpers
// File section
private void GetTextFromFile()
{
if (file.FilePath != null)
{
var fileStream = new FileStream(file.FilePath, FileMode.Open);
var streamReader = new StreamReader(fileStream, System.Text.Encoding.Default);
string data = null;
while ((data = streamReader.ReadLine()) != null)
{
_history.Add(data);
}
fileStream.Close();
streamReader.Close();
}
}
public void SaveChangesInFile()
{
if (file.FilePath != null)
{
File.WriteAllLines(file.FilePath, _history.ToList<string>());
}
}
private void SelectFile()
{
Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog();
dlg.DefaultExt = ".txt";
bool? result = dlg.ShowDialog();
if (result == true)
{
file.FilePath = dlg.FileName;
}
}
//TweetSharp section
private void SetUserName()
{
twitterUserModel.UserName = _someText.Substring(_someText.IndexOf("/", 15) + 1); //converting http to user's name. //magic numbers in mvvm??
}
private void ConvertHttpToId()
{
System.Windows.MessageBox.Show(twitterUserModel.UserName);
var user = service.GetUserProfileFor(new GetUserProfileForOptions
{
ScreenName = twitterUserModel.UserName
});
twitterUserModel.UserId = user.Id;
}
//History section
private void AddIdText()
{
AddToHistory(SomeText);
SomeText = string.Empty;
SaveChangesInFile();
}
private void AddHttpText()
{
if (string.IsNullOrWhiteSpace(SomeText)) return; // Checks if we have something in the field.
SetUserName(); // Converts http to user`s name.
ConvertHttpToId(); // Converts user's name to his id.
AddToHistory(twitterUserModel.UserId.ToString()); // Adds to history.
SomeText = string.Empty; // Deletes text in the input field
SaveChangesInFile(); // Saves history to the file.
}
private void ClearHistory()
{
_history.Clear();
}
private void AddToHistory(string item)
{
if (!_history.Contains(item))
_history.Add(item);
}
private void DeleteLineHistory()
{
if (_selectedLineIndex != null && _selectedLineIndex >= 0)
{
_history.RemoveAt((int)_selectedLineIndex);
}
}
#endregion
}
}
Models:
namespace MinimalMVVM.Models
{
public class TwitterUserModel : ObservableObject
{
private string _userName;
public string UserName
{
get { return _userName; }
set { _userName = value; }
}
private long _userId;
public long UserId
{
get { return _userId; }
set { _userId = value; }
}
}
}
namespace MinimalMVVM.Models
{
public class FileModel : ObservableObject
{
string _filePath;
public string FilePath
{
get { return _filePath; }
set { _filePath = value;
RaisePropertyChangedEvent("FilePath");
}
}
}
}
1) Во вьюмодели не надо использовать Microsoft.Win32.OpenFileDialog
или вызывать MessageBox
. Вы должны создать интерфейс с объявлением нужных вам методов и создать класс, который будет реализовывать для вас вызов окон и прочего визуального (на крайний случай можно сделать реализацию этого интерфейса прямо в классе Window).
Например
public interface IWindowsController
{
void ShowMessage(string message);
}
Потом допишем в кодбихайнде окна
public partial class MainWindow : Window, IWindowsController
и реализуем нужный метод
public void ShowMessage(string message)
{
MessageBox.Show(message);
}
Во вьюмодель же вы будете передавать ссылку на реализацию через конструктор
public MainViewModel(IWindowsController windowsController)
{
if (windowsController == null) throw new ArgumentNullException(nameof(windowsController));
_windowsController = windowsController;
}
Правда для этого вам придется убрать привязку вьюмодели через XAML, а сделать это в коде, например в том же конструкторе окна
MainViewModel vm = new MainViewModel(this);
this.DataContext = vm;
Ну или еще делают так называемый ViewModelLocator - отдельный класс, в котором реализуют запуск окон и их привязку к вьюмоделям.
Тогда чтоб показать сообщение из вьюмодели достаточно написать это
_windowsController.ShowMessage("Привет");
2) Вот этот шаблон для вызова события
protected void RaisePropertyChangedEvent(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
несколько устарел. Лучше так
//атрибут требует добавления using System.Runtime.CompilerServices;
protected void OnPropertyChanged([CallerMemberName]string propertyName = null)
{
// ? - потокобезопасно проверяет есть ли подписанты на это событие
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(propertyName)));
}
тогда при вызове не нужно указывать название свойства
OnPropertyChanged();
а если все-таки нужно указать название свойства, то делайте это так
OnPropertyChanged(nameof(MyProperty));
это подстрахует вас на случай, если вы захотите потом переименовать это свойство в ходе рефакторинга.
P.S. Вот это плохой код
private void DeleteLineFromText()
{
try
{
List<string> linesList = File.ReadAllLines(file.FilePath).ToList();
linesList.RemoveAt((int)_selectedLineIndex);
File.WriteAllLines(file.FilePath, _history);
}
catch (System.ArgumentNullException) { }
catch (System.ArgumentOutOfRangeException) { }
DeleteLineHistory();
SaveChangesInFile();
}
пустые блоки catch
- это то, что реально поможет вам напороться на ситуацию падения вашей программы неизвестно по какой причине.
Да, и что будет, если файл по какой-то причине не получиться прочитать или записать? Случится ведь другое исключение не из тех, что вы сейчас ожидаете.
Современные решения для бизнеса: как облачные и виртуальные технологии меняют рынок
Виртуальный выделенный сервер (VDS) становится отличным выбором
Такая проблема: хочу сделать гиперссылку, которая не будет открывать страницу в интернете, а просто запустит методКак Button запускает метод...
Мне нужно на сайте сделать ротатор баннеровТо есть, на одно место клиент ставит 3 баннера и говорит: 1-й - 20%, 2-й - 30%, 3-й - 50%