Форум программистов, компьютерный форум, киберфорум
Наши страницы
Элд Хасп
Войти
Регистрация
Восстановить пароль
Оценить эту запись

Пример реализации WPF+MVVM приложения

Запись от Элд Хасп размещена 26.12.2018 в 06:00

Пример решения ТЗ по теме Нечеткий поиск
В теме поставлена задача реализовать: поиск слов по списку слов, отображение списка слов, дополнение списка слов.

В любом решении WPF используются интерфейсы ICommand для команд и интерфейс INotifyPropertyChanged (сокращёно INPC) для свойств ViewModel. Поэтому в моих решениях всегда есть два класса:
RelayCommand - реализующий интерфейс ICommand
Кликните здесь для просмотра всего текста
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    public class RelayCommand : ICommand
    {
        public event EventHandler CanExecuteChanged;
        private readonly Func<object, bool> _canExecute;
        private readonly Action<object> _onExecute;
 
        /// <summary>Конструктор команды</summary>
        /// <param name="execute">Выполняемый метод команды</param>
        /// <param name="canExecute">Метод разрешающий выполнение команды</param>
        public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
        {
            _onExecute = execute;
            _canExecute = canExecute;
        }
 
        /// <summary>Вызов разрешающего метода команды</summary>
        /// <param name="parameter">Параметр команды</param>
        /// <returns>True? если выполнение команды разрешено</returns>
        public bool CanExecute(object parameter) => _canExecute == null ? true : _canExecute.Invoke(parameter);
 
        /// <summary>Вызов выполняющего метода команды</summary>
        /// <param name="parameter">Параметр команды</param>
        public void Execute(object parameter) => _onExecute?.Invoke(parameter);
    }
OnPropertyChangedClass - реализующий интерфейс INotifyPropertyChanged
Кликните здесь для просмотра всего текста
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
    /// <summary>Базовый класс с реализацией INPC </summary>
    public class OnPropertyChangedClass : INotifyPropertyChanged
    {
        /// <summary>Событие для извещения об изменения свойства</summary>
        public event PropertyChangedEventHandler PropertyChanged;
 
        /// <summary>Метод для вызова события извещения об изменении свойства</summary>
        /// <param name="prop">Изменившееся свойство или список свойств через разделители "\\/\r \n()\"\'-"</param>
        public void OnPropertyChanged([CallerMemberName]string prop = "")
        {
            string[] names = prop.Split("\\/\r \n()\"\'-".ToArray(), StringSplitOptions.RemoveEmptyEntries);
            if (names.Length != 0)
                foreach (string _prp in names)
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(_prp));
        }
 
        /// <summary>Метод для вызова события извещения об изменении списка свойств</summary>
        /// <param name="propList">Последовательность имён свойств</param>
        public void OnPropertyChanged(IEnumerable<string> propList)
        {
            foreach (string _prp in propList.Where(name => !string.IsNullOrWhiteSpace(name)))
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(_prp));
        }
 
        /// <summary>Метод для вызова события извещения об изменении списка свойств</summary>
        /// <param name="propList">Последовательность свойств</param>
        public void OnPropertyChanged(IEnumerable<PropertyInfo> propList)
        {
            foreach (PropertyInfo _prp in propList)
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(_prp.Name));
        }
 
        /// <summary>Метод для вызова события извещения об изменении всех свойств</summary>
        public void OnAllPropertyChanged() => OnPropertyChanged(GetType().GetProperties());
    }

В паттерне MVVM создание приложения начинается с создания Model (Модели). В нашем случае в Модели должны быть реализованы методы и свойство:
  • Чтение списка слов из файла
  • Сохранение списка слов в файле
  • Пополнение списка слов
  • Поиск по списку слов с возвратом результата поиска
  • Свойство возвращающие коллекцию из списка слов. Так как коллекция предназначена для привязки к WPF элементу, то она должна поддерживать интерфейс INotifyCollectionChanged (сокращёно INCC).
Во многих случаях для Модели удобно выбрать статический класс. Так же сделаем и для этого решения.
Класс Модели - FuzzySearch_Model
Кликните здесь для просмотра всего текста
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
    /// <summary>Статический класс модели данных</summary>
    public static class FuzzySearch_Model
    {
        static ObservableCollection<string> _listWords;
        /// <summary>Список слов</summary>
        public static ObservableCollection<string> ListWords
        {
            get => _listWords ?? (_listWords = new ObservableCollection<string>());
            private set => _listWords = value;
        }
 
        /// <summary>Сохранение списка слов в файле</summary>
        /// <param name="NameFile">Имя файла</param>
        public static void Save(string NameFile = null)
        {
            if (string.IsNullOrWhiteSpace(NameFile))
                NameFile = "ListWords.txt";
            StreamWriter file = null;
            try
            {
                file = new StreamWriter(NameFile, false, Encoding.Default);
                foreach (string word in ListWords)
                    file.WriteLine(word.ToLower());
            }
            catch (Exception)
            {
                MessageBox.Show("Ошибка записи в файл!");
            }
            if (file != null)
                file.Close();
        }
 
        /// <summary>Чтение списка слов из файла</summary>
        /// <param name="NameFile">Имя файла</param>
        public static void Load(string NameFile = null)
        {
            if (string.IsNullOrWhiteSpace(NameFile))
            {
                NameFile = "ListWords.txt";
                if (!File.Exists(NameFile))
                    File.Create(NameFile).Close();
            }
            StreamReader file = null;
            try
            {
                file = new StreamReader(NameFile, Encoding.Default);
                ListWords = new ObservableCollection<string>(file.ReadToEnd().Split("\r\n ".ToCharArray(), StringSplitOptions.RemoveEmptyEntries).Select(wrd => wrd.ToLower()));
            }
            catch (Exception)
            {
                MessageBox.Show("Ошибка чтения из файла!");
            }
            if (file != null)
                file.Close();
        }
 
        /// <summary>Добавление слова в список</summary>
        /// <param name="Words">Слово или список слов разделённых переносом строки или пробелом</param>
        public static void AddWord(string Words)
        {
            foreach (string word in Words.Split("\r\n ".ToCharArray(), StringSplitOptions.RemoveEmptyEntries))
                ListWords.Add(word);
        }
 
        /// <summary>Поиск слова в списке</summary>
        /// <param name="Word">Одно слово</param>
        /// <returns>Результат поиска</returns>
        public static string SearchWord(string Word)
        {
            string[] wordArr = Word.ToLower().Split("\r\n ".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
            if (wordArr.Length != 1) return "Ошибка ввода";
            Word = wordArr[0];
            int index = ListWords.IndexOf(Word);
            if (index >= 0) return "Tочное совпадение со словом по индексу " + index;
            /// *************************
            /// Нечёткий поиск
            /// *************************
            return "Результат нечёткого поиска";
        }
    }
После создания Модели создаётся View (Представление). Задачей View является ОТОБРАЖЕНИЕ данных. Ни какой обработки данных в View быть не должно! В отображение данных входит непосредственно показ самих данных, редактирование значений данных, WPF конвертация данных в удобный для отображения вид, команды для элементов (в первую очередь для кнопок и меню).
При создании View элементы надо к чему-то привязывать. Создавать абстрактные привязки, в которых на момент редактирования ничего нет, визуально трудно и приводит к многочисленным ошибкам. Поэтому перед созданием View надо создать ViewModel DesignData (Модель представления времени разработки). Она может быть как единая для всей View, так и раздробленная - отдельная для каждого элемента. И надо понимать VMDD - это часть View, а не VM. И заполняется данными она в XAML. Изменяя эти данные, очень удобно видеть как меняется View. Особенно в случаях когда в зависимости от данных должны скрываться/показываться разные элементы.
VM, в дальнейшем, удобно наследовать от VMDD. А так как VM должна поддерживать интерфейс INPC, класс VMDD будем наследовать от класса OnPropertyChangedClass. Для привязки команд в VMDD будем использовать класс RelayCommand.
Класс VMDD - FuzzySearch_VM_DataDesigner
Кликните здесь для просмотра всего текста
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
    /// <summary>Класс ViewModel for DesignData</summary>
    public class FuzzySearch_VM_DataDesigner : OnPropertyChangedClass
    {
        #region Свойства для привязки элементов отображения
        /// <summary>Свойство для слова поиска</summary>
        public string TextSearch { get => _textSearch; set { _textSearch = value; OnAllPropertyChanged(); } }
        /// <summary>Свойство для слова добавления</summary>
        public string TextAdd { get => _textAdd; set { _textAdd = value; OnAllPropertyChanged(); } }
        /// <summary>Свойство для списка слова</summary>
        public ObservableCollection<string> ListWords { get => _listWords; set { _listWords = value; OnAllPropertyChanged(); } }
        /// <summary>Свойство для сообщения с результатами поиска</summary>
        public string TextMessage { get => _textMessage; set { _textMessage = value; OnAllPropertyChanged(); } }
        #endregion
 
        #region Приватные поля
        // Поля для хранения значения свойства
        private string _textSearch;
        private string _textAdd;
        private ObservableCollection<string> _listWords;
        private string _textMessage;
        // Поля для хранения значения команд
        private ICommand _searchComm;
        private ICommand _clearComm;
        private ICommand _addComm;
        private ICommand _saveComm;
        private ICommand _loadComm;
        #endregion
 
        #region Свойства для привязки команд
        /// <summary>Свойство для привязки команды</summary>
        public ICommand SearchComm => _searchComm ?? (_searchComm = new RelayCommand(OnSearch));
        /// <summary>Свойство для привязки команды</summary>
        public ICommand ClearComm => _clearComm ?? (_clearComm = new RelayCommand(OnClear));
        /// <summary>Свойство для привязки команды</summary>
        public ICommand AddComm => _addComm ?? (_addComm = new RelayCommand(OnAdd));
        /// <summary>Свойство для привязки команды</summary>
        public ICommand SaveComm => _saveComm ?? (_saveComm = new RelayCommand(OnSave));
        /// <summary>Свойство для привязки команды</summary>
        public ICommand LoadComm => _loadComm ?? (_loadComm = new RelayCommand(OnLoad));
        #endregion
 
        #region Методы для команд
        /// <summary>Метод для вызова из команды</summary>
        /// <param name="Value">Значение привязанного параметра</param>
        public virtual void OnSearch(object Value = null) { }
        /// <summary>Метод для вызова из команды</summary>
        /// <param name="Value">Значение привязанного параметра</param>
        public virtual void OnClear(object Value = null)
        { TextSearch = ""; TextMessage = ""; TextAdd = ""; }
        /// <summary>Метод для вызова из команды</summary>
        /// <param name="Value">Значение привязанного параметра</param>
        public virtual void OnAdd(object Value = null) { }
        /// <summary>Метод для вызова из команды</summary>
        /// <param name="Value">Значение привязанного параметра</param>
        public virtual void OnSave(object Value = null) { }
        /// <summary>Метод для вызова из команды</summary>
        /// <param name="Value">Значение привязанного параметра</param>
        public virtual void OnLoad(object Value = null) { }
        #endregion
    }
Заполняя данными VMDD в Xaml окна - используем префикс d:, который сообщает компилятору, что это данные времени разработки и компилировать в сборку их не надо. Для развитой VMDD для отладки View временно убирается префикс d: и исполняется проект. View должна быть полностью рабочей для такого исполнения! Допустим, в нашем случае, запуская окно мы можем изменять его размеры и видеть как адаптируются размеры элементов под размеры окна. Исходя из этого мы можем ограничить максимальные или минимальные размеры окна.
XML
1
2
3
4
5
6
7
8
9
    <d:Window.DataContext>
        <local:FuzzySearch_VM_DataDesigner TextSearch="Слово для поиска" TextAdd="Слово для добавления" TextMessage="Сообщение">
            <local:FuzzySearch_VM_DataDesigner.ListWords>
                <sys:String>Анекдоты</sys:String>
                <sys:String>про</sys:String>
                <sys:String>Вовочку</sys:String>
            </local:FuzzySearch_VM_DataDesigner.ListWords>
        </local:FuzzySearch_VM_DataDesigner>
    </d:Window.DataContext>
Теперь можно удобно создавать WPF View. Одна из частых ошибок для начинающих, особенно тех кто до этого работал в WinForms, "жёстко" позиционировать элементы с помощью свойств Margin и размера элементов. Для WPF такое НЕПРИЕМЛЕМО! Margin это свойство для задания расстояния между элементами, а не их позиции. Для компоновки элементов надо использовать различные типы контейнеров.
Для компоновки, в данном случае, хватает комбинации панелей Grid и StackPanel. Все элементы сразу привязываем к данным. Так как у нас есть заполненная VMDD, то в конструкторе всплывают свойства VMDD и в элементах сразу отображаются значения этих свойств.
Xaml элементов окна
Кликните здесь для просмотра всего текста
XML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    <Grid x:Name="Can1" ShowGridLines="False" Background="Aqua">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <StackPanel>
            <TextBox TextWrapping="Wrap" Text="{Binding TextSearch}" Margin="10"/>
            <Button Content="Поиск"  VerticalAlignment="Bottom" Margin="10" Command="{Binding SearchComm}"/>
        </StackPanel>
        <TextBlock Grid.Column="1" Margin="10" TextWrapping="Wrap" Background="White" Text="{Binding TextMessage}"/>
        <TextBox TextWrapping="Wrap" Text="{Binding TextAdd}" Margin="10" Grid.Row="2"/>
        <ListBox Margin="10" Grid.Row="1" MinHeight="20" Grid.ColumnSpan="3" ItemsSource="{Binding ListWords}"/>
        <Button  Content="Очистить"  VerticalAlignment="Bottom" Margin="10" Grid.Row="0" Grid.Column="2" Command="{Binding ClearComm}"/>
        <Button Content="Добавить"  VerticalAlignment="Center" Margin="10" Grid.Row="2" Grid.Column="1" Command="{Binding AddComm}"/>
        <StackPanel Grid.Row="2" Grid.Column="2">
            <Button Content="Загрузить" Margin="10" Command="{Binding LoadComm}"/>
            <Button Content="Сохранить" Margin="10" Command="{Binding SaveComm}"/>
        </StackPanel>
    </Grid>

После создания View надо создать реальную VM. В ней нужно прописать связь с Моделью. Так как у нас уже есть VMDD, то VM будем наследовать от неё. Нам понадобится прописать в конструкторе инициализацию Модели и свойства для списка слов и переопределить методы для кнопок.
Класс VM - FuzzySearch_VM
Кликните здесь для просмотра всего текста
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
    /// <summary>Класс реальной ViewModel</summary>
    public class FuzzySearch_VM : FuzzySearch_VM_DataDesigner
    {
        public FuzzySearch_VM()
        {
            FuzzySearch_Model.Load();
            ListWords = FuzzySearch_Model.ListWords;
        }
 
        public override void OnAdd(object Value = null) => FuzzySearch_Model.AddWord(TextAdd);
        public override void OnLoad(object Value = null) => FuzzySearch_Model.Load();
        public override void OnSave(object Value = null) => FuzzySearch_Model.Save();
        public override void OnSearch(object Value = null) => TextMessage = FuzzySearch_Model.SearchWord(TextSearch);
    }

Теперь осталось только подключить VM к View
XML
1
2
3
    <Window.DataContext>
        <local:FuzzySearch_VM/>
    </Window.DataContext>
На этом всё - наше WPF приложение в паттерне MVVM создано!
Архив проекта - приложен.
Вложения
Тип файла: 7z FuzzySearch.7z (28.3 Кб, 24 просмотров)
Размещено в WPF
Просмотров 223 Комментарии 1
Всего комментариев 1
Комментарии
  1. Старый комментарий
    Аватар для defer
    Почему-то если сделать новый проект, то окно не заполняется строками

    C#
    1
    2
    3
    
    <sys:String>Анекдоты</sys:String>
                    <sys:String>про</sys:String>
                    <sys:String>Вовочку</sys:String>
    Запись от defer размещена 31.12.2018 в 19:01 defer вне форума
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2019, vBulletin Solutions, Inc.
Рейтинг@Mail.ru