Пример решения ТЗ по теме Нечеткий поиск
В теме поставлена задача реализовать: поиск слов по списку слов, отображение списка слов, дополнение списка слов.
В любом решении 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 к ViewXML | 1
2
3
| <Window.DataContext>
<local:FuzzySearch_VM/>
</Window.DataContext> |
|
На этом всё - наше WPF приложение в паттерне MVVM создано!
Архив проекта - приложен. |