Критика кода. MVVM, WPF, C#

140
05 мая 2019, 18:00

Решил для себя разобраться с MVVM, не совсем уверен правильно ли я организовал связь между Model и ViewModel. Хотел бы услышать ваше мнение по поводу реализации этого паттерна, а также претензии к коду.

Функционал программы на данный момент:

  • Открытие файла;
  • Добавление строк в файл;
  • Удаление строк из файла;
  • Конвертация ссылки пользователя твиттера (библиотека TweetSharp) на его ID.

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");
            }
        }
    }
}
Answer 1

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 - это то, что реально поможет вам напороться на ситуацию падения вашей программы неизвестно по какой причине. Да, и что будет, если файл по какой-то причине не получиться прочитать или записать? Случится ведь другое исключение не из тех, что вы сейчас ожидаете.

READ ALSO
Для чего нужны свойства?

Для чего нужны свойства?

Допустим есть это:

129
C# WPF: Hyperlink без NavigateUri

C# WPF: Hyperlink без NavigateUri

Такая проблема: хочу сделать гиперссылку, которая не будет открывать страницу в интернете, а просто запустит методКак Button запускает метод...

158
Yii2: ротатор баннеров

Yii2: ротатор баннеров

Мне нужно на сайте сделать ротатор баннеровТо есть, на одно место клиент ставит 3 баннера и говорит: 1-й - 20%, 2-й - 30%, 3-й - 50%

144