WPF&MVVM: Ввод начальных данных в Model

307
08 октября 2017, 22:06

Как это было отмечено в одном из комментариев к вопросу Ввод данных во ViewModel, хранение данных во ViewModel является противоречием шаблону MVVM, но во всех уроках по MVVM для начинающих, которые я видел, в целях урощения ввод данных осуществляется именно во ViewModel.

В своей первый попытке получить данные из модели я взял за основу я взял код из уроков на сайте metanit.com (полный код вставлять в вопрос не буду; он доступен в исходниках):

MainWindow.xaml

Phoce.cs (Model)

namespace MVVM_GetDataFromTextFileToModel_Test {
    public class Phone : INotifyPropertyChanged {
        private string title;
        private string company;
        private int price;
        public string Title {
            get { return title; }
            set {
                title = value;
                OnPropertyChanged("Title");
            }
        }
        public string Company {
            get { return company; }
            set {
                company = value;
                OnPropertyChanged("Company");
            }
        }
        public int Price {
            get { return price; }
            set {
                price = value;
                OnPropertyChanged("Price");
            }
        }
        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged([CallerMemberName]string prop = "") {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(prop));
        }
    }
}

Наконец, ввод начальных данных осуществляется во ViewModel, а имеено в её конструкторе:

public ApplicationViewModel() {
    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 }
    };
}

Я рассуждал так, что если для XAML нужна ObservableCollection, значит мы должны создать её внутри модели. Но получается, что для объявления данной переменной надо сослаться на тот же класс, в котором мы переменную и объявляем:

public ObservableCollection<Phone> phones;

Пока IDE не даёт никаких предупреждений. Далее я не понял:

  • Как следует описать setter в свойстве Phones?

    public ObservableCollection<Phone> Phones {
        get { return phones; }
        set {
        }
    }
    
  • Где именно внутри модели лучше ввести начальные данные?

  • Как лучше при создании экземпляра класса Phone во ViewModel организовать получение коллекции данных?

Ссылка на исходники (Яндекс Диск; возможно станет недоступна после получения ответа на вопрос)

Обновление

Добавил в модель метод, возвращающий ObservableCollection<Phone> и вызвал его в конструкторе ApplicationViewModel. Приложение собирается, но данных никаких не выводит.

Phone.cs

public ObservableCollection<Phone> getPhones() {
    return 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 }
    };
}

ApplicationViewModel

public class ApplicationViewModel : INotifyPropertyChanged {
    public ObservableCollection<Phone> Phones;
    // ... 
    public ApplicationViewModel() {
        Phone phone = new Phone();
        Phones = phone.getPhones();
    }    
    // ...    
}

Кстати, мне не совсем понятна конструкция в ApplicationViewModel при объявлении полей класса, которая была до этого:

public ObservableCollection<Phone> Phones { get; set; }

Мы объявляем коллекцию, но при этом объявляем методы get и set? Если так всё и оставить, то мой исправленный код не скомплируется, поэтому я оставил только public ObservableCollection<Phone> Phones;.

Answer 1
  • Phone.cs без изменений - это сущностной класс.
  • В PhoneRepository.cs находятся данные телефонов (на следующем этапе обучения их нужно будет взять из внешних источников, например БД). Метод getAllPhones() возвращает все телефоны в виде ObservableCollection<Phone>; метод getPhoneByName(string phoneName) возвращает телефон по имени (я не переименовал свойство Title, когда брал за основу код с урока на metanit.com).

    class PhoneRepository {
        private ObservableCollection<Phone> phones = new ObservableCollection<Phone> {
            new Phone {Title="iPhone7", 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 ObservableCollection<Phone> GetAllPhones(){
            return phones;
        }
        public Phone GetPhoneByName(string phoneName) {
            foreach (var phone in phones) {
                if (phone.Title == phoneName) {
                    return phone;
                }
            }
            return null;
        }
    }
    
  • В ApplicationViewModel необходимо изменить объявление Phones, потому что как сказал @Андрей в комментариях, привязка работает только со свойствами, но не полями.

    public class ApplicationViewModel : INotifyPropertyChanged {
        public ObservableCollection<Phone> Phones { get; } =
            new ObservableCollection<Phone>();
        // ...
        public ApplicationViewModel() {
            PhoneRepository phoneRepository = new PhoneRepository();
            Phones = phoneRepository.GetAllPhones();
        }
    }
    
Answer 2

а как тогда быть с наследованием INotifyPropertyChanged? Без него не будет работать OnPropertyChanged

Вот вам готовый ответ на ваш вопрос.

1) Опишем для наглядности интерфейс для всего и вся так сказать

using System.Windows;
namespace MVVMTest.Services.Interfaces
{
    interface IModel
    {
        /// <summary>
        /// Добавим более удобный метод для возврата значений из DependencyProperty.
        /// </summary>
        /// <typeparam name="T">Передадим тип, что бы не делать постоянный каст к необзодимому типу.</typeparam>
        /// <param name="dp">DependencyProperty из которого необходимо извлечь данные.</param>
        /// <returns>Возвращает текущее состояние DependencyProperty для заданного объекта.</returns>
        /// <exception cref="System.InvalidOperationException"/>
        T GetValue<T>(DependencyProperty dp);
    }
}

2) Добавим базовый класс для Models, и ViewModels:

using System.Windows;
namespace MVVMTest.Services.Base
{
    /// <summary>
    /// Базовый клас как для Models, так и для ViewModels
    /// </summary>
    class ModelBase : DependencyObject, IModel
    {
        /// <summary>
        /// Добавим более удобный метод для возврата значений из DependencyProperty.
        /// </summary>
        /// <typeparam name="T">Передадим тип, что бы не делать постоянный каст к необзодимому типу.</typeparam>
        /// <param name="dp">DependencyProperty из которого необходимо извлечь данные.</param>
        /// <returns>Возвращает текущее состояние DependencyProperty для заданного объекта.</returns>
        /// <exception cref="System.InvalidOperationException"/>
        public T GetValue<T>(DependencyProperty dp)
        {
            return (T)GetValue(dp);
        }
    }
}

3) Добавим базовый класс для ViewModels

namespace MVVMTest.Services.Base
{
    /// <summary>
    /// Базовый класс для ViewModels
    /// </summary>
    class ViewModelBase : ModelBase
    {
        /// <summary>
        /// Заголовок окна, например
        /// </summary>
        public virtual string Title { get; set; }
        // Больше не будем ничего пока добавлять, для примера сойдет
    }
}

4) Воспользуемся вашим классом RelayCommand

using System;
using System.Windows.Input;
namespace MVVMTest.Services.Command
{
    public class RelayCommand : ICommand
    {
        private Action<object> execute;
        private Func<object, bool> canExecute;
        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
        public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
        {
            this.execute = execute;
            this.canExecute = canExecute;
        }
        public bool CanExecute(object parameter)
        {
            return canExecute == null || canExecute(parameter);
        }
        public void Execute(object parameter)
        {
            execute(parameter);
        }
    }
}

5) Добавим MainViewModel

using MVVMTest.Models;
using MVVMTest.Services.Base;
using MVVMTest.Services.Command;
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
namespace MVVMTest.ViewModels
{
    class MainViewModel : ViewModelBase
    {
        private DispatcherTimer _dt;
        private int i = 0;
        public MainViewModel()
        {
            Phones = new ObservableCollection<PhoneModel>
            {
                new PhoneModel {Title="iPhone 7", Company="Apple", Price=56000 },
                new PhoneModel {Title="Galaxy S7 Edge", Company="Samsung", Price =60000 },
                new PhoneModel {Title="Elite x3", Company="HP", Price=56000 },
                new PhoneModel {Title="Mi5S", Company="Xiaomi", Price=35000 },
                new PhoneModel {Title="Galaxy S7 Edge", Company="Samsung", Price =60000 },
                new PhoneModel {Title="Elite x3", Company="HP", Price=56000 },
                new PhoneModel {Title="Galaxy S7 Edge", Company="Samsung", Price =60000 },
                new PhoneModel {Title="Elite x3", Company="HP", Price=56000 },
                new PhoneModel {Title="Galaxy S7 Edge", Company="Samsung", Price =60000 },
                new PhoneModel {Title="Elite x3", Company="HP", Price=56000 },
                new PhoneModel {Title="Galaxy S7 Edge", Company="Samsung", Price =60000 },
                new PhoneModel {Title="Elite x3", Company="HP", Price=56000 },
                new PhoneModel {Title="Galaxy S7 Edge", Company="Samsung", Price =60000 },
                new PhoneModel {Title="Elite x3", Company="HP", Price=56000 },
                new PhoneModel {Title="Galaxy S7 Edge", Company="Samsung", Price =60000 },
                new PhoneModel {Title="Elite x3", Company="HP", Price=56000 },
                new PhoneModel {Title="Galaxy S7 Edge", Company="Samsung", Price =60000 },
                new PhoneModel {Title="Elite x3", Company="HP", Price=56000 },
                new PhoneModel {Title="Galaxy S7 Edge", Company="Samsung", Price =60000 },
                new PhoneModel {Title="Elite x3", Company="HP", Price=56000 }
            };
            Title = "Заголовок главного окна успешно связан с ViewModel'ю!";
            _dt = new DispatcherTimer(TimeSpan.FromSeconds(1), DispatcherPriority.Background, ChangeTitle,
                Dispatcher.CurrentDispatcher);
            _dt.Start();
            RemoveCommand = new RelayCommand(obj =>
            {
                Phones.Remove(SelectedPhone);
                SelectedPhone = null;
            }, (obj) => SelectedPhone != null);
        }
        private void ChangeTitle(object sender, EventArgs e)
        {
            ++i;
            Title = $"Заголовок главного окна успешно установлен ViewModel'ю {i} раз!";
        }
        /// <summary>
        /// Переобпределим поле в базовом классе, т.к. мы булем делать привязку на окно, и будем его использовать в качестве значения Title для окна
        /// </summary>
        public override string Title
        {
            get { return GetValue<string>(TitleProperty); }
            set { SetValue(TitleProperty, value); }
        }
        public static readonly DependencyProperty TitleProperty = DependencyProperty.Register("Title", typeof(string), typeof(ViewModelBase));
        ~MainViewModel()
        {
            _dt?.Stop();
        }

        public ObservableCollection<PhoneModel> Phones
        {
            get { return GetValue<ObservableCollection<PhoneModel>>(PhonesProperty); }
            set { SetValue(PhonesProperty, value); }
        }
        // Using a DependencyProperty as the backing store for Phones.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PhonesProperty =
            DependencyProperty.Register("Phones", typeof(ObservableCollection<PhoneModel>), typeof(MainViewModel));

        public PhoneModel SelectedPhone
        {
            get { return GetValue<PhoneModel>(SelectedPhoneProperty); }
            set { SetValue(SelectedPhoneProperty, value); }
        }
        // Using a DependencyProperty as the backing store for SeletedPhone.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SelectedPhoneProperty =
            DependencyProperty.Register("SeletedPhone", typeof(PhoneModel), typeof(MainViewModel));

        public ICommand AddPhone
        {
            get { return GetValue<ICommand>(AddPhoneProperty); }
            set { SetValue(AddPhoneProperty, value); }
        }
        // Using a DependencyProperty as the backing store for AddPhone.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty AddPhoneProperty =
            DependencyProperty.Register("AddPhone", typeof(ICommand), typeof(MainViewModel));

        public ICommand RemoveCommand
        {
            get { return GetValue<ICommand>(RemoveCommandProperty); }
            set { SetValue(RemoveCommandProperty, value); }
        }
        // Using a DependencyProperty as the backing store for RemoveCommand.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty RemoveCommandProperty =
            DependencyProperty.Register("RemoveCommand", typeof(ICommand), typeof(MainViewModel));
    }
}

6) Добавим PhoneModel

using MVVMTest.Services.Base;
using System.Windows;
namespace MVVMTest.Models
{
    class PhoneModel : ModelBase
    {
        public PhoneModel()
        {
        }
        public PhoneModel(PhoneModel mainModel)
        {
            Title = mainModel.Title;
            Company = mainModel.Company;
            Price = mainModel.Price;
        }
        public string Title
        {
            get { return GetValue<string>(TitleProperty); }
            set { SetValue(TitleProperty, value); }
        }
        // Using a DependencyProperty as the backing store for Title.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty TitleProperty =
            DependencyProperty.Register("Title", typeof(string), typeof(PhoneModel));
        public string Company
        {
            get { return GetValue<string>(CompanyProperty); }
            set { SetValue(CompanyProperty, value); }
        }
        // Using a DependencyProperty as the backing store for Company.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty CompanyProperty =
            DependencyProperty.Register("Company", typeof(string), typeof(PhoneModel));
        public int Price
        {
            get { return GetValue<int>(PriceProperty); }
            set { SetValue(PriceProperty, value); }
        }
        // Using a DependencyProperty as the backing store for Price.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PriceProperty =
            DependencyProperty.Register("Price", typeof(int), typeof(PhoneModel));
    }
}

7) Переопределим метод OnStartup в App.xaml.cs

using System.Windows;
using MVVMTest.Views;
using MVVMTest.ViewModels;
namespace MVVMTest
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            // Выполним базовые действия для Application, поле чего запустим наше окно.
            base.OnStartup(e);
            new MainView { DataContext = new MainViewModel() }.Show();
        }
    }
}

8) Добавим MainView, для совместимости, постарался ничего глобально не менять.

<Window x:Class="MVVMTest.Views.MainView"
        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"
        mc:Ignorable="d"
        WindowStartupLocation="CenterScreen"
        Title="{Binding Title}" Height="480" Width="640">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="0.8*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="0.2*" />
        </Grid.RowDefinitions>
        <ListBox Grid.Column="0" 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="1" Orientation="Horizontal">
            <Button Command="{Binding AddCommand}">+</Button>
            <Button Command="{Binding RemoveCommand}">-</Button>
        </StackPanel>
        <StackPanel 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>

9) Код MainView.xaml.cs

using System.Windows;
namespace MVVMTest.Views
{
    public partial class MainView : Window
    {
        public MainView()
        {
            InitializeComponent();
        }
    }
}

Запустим, поглядим...

Для того что бы понять как это все работает, добавляю архив с исходный кодом Yandex.Disk

READ ALSO
WPF: Как правильно с точки зрения концепции MVVM вызывать новое окно командой ?

WPF: Как правильно с точки зрения концепции MVVM вызывать новое окно командой ?

Без шаблона MVVM, вызов нового окна в приложениях WPF довольно прост:

261
Не удается подключиться к серверу со статическим IP

Не удается подключиться к серверу со статическим IP

ПриветсвтуюНаписал на c# простое приложение клиент-сервер

271
C# Как отредактировать текст, выведенный в консоль

C# Как отредактировать текст, выведенный в консоль

Помогите пожалуйстаМне нужно, чтобы пользователь мог вводить символы (на месте курсора), и введенные данные отображались в том же месте (на месте...

232
Как собрать .NET Framework

Как собрать .NET Framework

https://githubcom/microsoft/referencesource

252