Проекты
December 18, 2018

Программа для добавления/удаления строки к именам файлов в папке. Часть 2.

Теперь рассмотрим процесс реализации класса ViewModel, наследуемого от ViewModelBase. Это абстрактный класс, который содержится в библиотеке MVVM Light. С помощью него реализуется обновление данных в свойствах, привязанных к элементам UI, а так же возможность передачи сообщений между объектами.

Начнем с описания логики работы первой вкладки программы «Добавление к имени файла»

Реализация добавления строки к имени файла.

Во-первых нам нужно связать TextBox’ы с соответствующими свойствами в MainViewModel через алгоритм привязки.

xaml

<!--Путь до папки с файлами-->
<TextBox Grid.Column="0" Name="PathToFolderAddString_tb" 
   Text="{Binding PathToFilesForAddString_Renamer, Mode=TwoWay,
   UpdateSourceTrigger=PropertyChanged}"/>
<!--Строка до имени файла-->
<TextBox Name="AddTextBefore_tb" 
   Text="{Binding StringBeforeFileName_Renamer, 
   UpdateSourceTrigger=PropertyChanged}"/>
<!--Строка после имени файла-->
<TextBox Name="AddTextAfter_tb" 
   Text="{Binding StringAfterFileName_Renamer, 
   UpdateSourceTrigger=PropertyChanged}"/>
<!--Строка в имени файла-->
<TextBox Name="InsertTextOnPosition_td" 
   Text="{Binding InsertTextOnPosition_Renamer, 
   UpdateSourceTrigger=PropertyChanged}"/>

C#

//Путь до папки в которой переименовываются файл
private string _pathToFilesForAddString_Renamer;
public string PathToFilesForAddString_Renamer
  {
    get { return _pathToFilesForAddString_Renamer; }
    set
      {
        _pathToFilesForAddString_Renamer = value;
        //Если меняется путь, то автоматическое 
        //слежение за папкой выключается
        if (AutoWatchingForFolder)
        {
          AutoWatchingForFolder = false;
        }
        RaisePropertyChanged(nameof(PathToFilesForAddString_Renamer));
      }
   }
//Строка которую нужно вставить до имени файла
private string _stringBeforeFileName_Renamer = "";
public string StringBeforeFileName_Renamer
  {
   get { return _stringBeforeFileName_Renamer; }
   set
    {
      _stringBeforeFileName_Renamer = value;
      RaisePropertyChanged(nameof(StringBeforeFileName_Renamer));
    }
  }
//Строка которую нужно вставить после имени файла
private string _stringAfterFileName_Renamer = "";
public string StringAfterFileName_Renamer
  {
    get { return _stringAfterFileName_Renamer; }
    set
    {
      _stringAfterFileName_Renamer = value;
      RaisePropertyChanged(nameof(StringAfterFileName_Renamer));
    }
  }
//Строка которую нужно вставить на определенную позицию
private string _insertTextOnPosition_Renamer = "";
public string InsertTextOnPosition_Renamer
  {
    get { return _insertTextOnPosition_Renamer; }
    set
    {
      _insertTextOnPosition_Renamer = value;
      RaisePropertyChanged(nameof(InsertTextOnPosition_Renamer));
    }
  }

Так как в WPF нет элемента NumericUpDown, пришлось его искать в сторонних библиотеках. Выбор пал на Xceed, она есть как платная, так и бесплатная. Подключить ее можно через менеджер пакетов NuGet. Чтобы использовать ее элементы, нам нужно прописать:

xaml

xmlns:xctk=”http://schemas.xceed.com/wpf/xaml/toolkit”

После этого можно создать сам элемент и привязать его к свойству

xaml

<xctk:IntegerUpDown Minimum="0" Margin="10 5 0 5" 
  Text="{Binding InputPosition_Renamer, 
  UpdateSourceTrigger=PropertyChanged}"/>

C#

//Позиция начиная с которой нужно вставить строку
private int _inputPosition_Renamer = 0;
public int InputPosition_Renamer
    {
      get { return _inputPosition_Renamer; }
      set
      {
        _inputPosition_Renamer = value;
        RaisePropertyChanged(nameof(InputPosition_Renamer));
      }
    }

Помимо технологии привязки данных, в WPF предусмотрено связывание действий элементов Button с методами класса ViewModel через систему команд. Для этого существует вспомогательный класс RelayCommand, наследуемый от интерфейса ICommand. Его реализация уже встроена в библиотеку MVVM Light, но его можно реализовать и самостоятельно. (Если нужна будет реализация напишу в комментариях).

Для того чтобы связать команду и визуальный элемент напишем следующее:

xaml

<Button Name="RenameFilesOneTime_btn" 
  Command="{Binding RenameFiles_Renaimer_Command}" 
  Content="Переименовать файлы" />

C#

//Команда для переименовывания файлов
private RelayCommand _renameFiles_Renamer_Command;
public RelayCommand RenameFiles_Renaimer_Command
    {
      get
      {
        return _renameFiles_Renamer_Command ?? 
          (_renameFiles_Renamer_Command = new RelayCommand(() =>
        {
          RenameFilesMethod_Renamer(PathToFilesForAddString_Renamer);
        }));
      }
    }

Метод RenameFilesMethod_Renamer(string pathToFolder) принимает на вход путь до папки, в которой лежат файлы.

//Метод для переименовывания файлов в папке
private void RenameFilesMethod_Renamer(string pathToFolder)
 {
   if (CheckEnteredPath(pathToFolder) == 0)
   {
     return;
   }

   //Процесс переименования
   IEnumerable<FileInfo> filesToRename = Directory.GetFiles(pathToFolder)
                                            .Select(f => new FileInfo(f));
   foreach (FileInfo fileInfo in filesToRename)
   {
     string newFileName = RenameFileMethod_Renamer(fileInfo);
     if (newFileName == "errorExceptionCheckIt")
     {
       return;
     }
     Messenger.Default.Send(new AddInfoInLogFileMessage()
     {
       MessageForLogFile = "Файл " + fileInfo.Name + " переименован в " + newFileName,
       TextColorInLog = "#000000"
     });
   }
 }

Метод RenameFileMethod_Renamer(FileInfo inputFile) сделан для удобства, потому что позже будет реализован алгоритм постоянного сканирования папки на наличие новых файлов и этот метод пригодится.

//Метод для переименовывания одного файла
private string RenameFileMethod_Renamer(FileInfo inputFile)
 {
   try
   {
    string newFileName;
    newFileName = $@"{StringBeforeFileName_Renamer}{
               Path.GetFileNameWithoutExtension(inputFile.Name).Insert(InputPosition_Renamer,
               InsertTextOnPosition_Renamer)
             }" +
            $@"{StringAfterFileName_Renamer}{inputFile.Extension}";
    string newFileFullPath = Path.Combine(inputFile.DirectoryName, newFileName);
    File.Move(inputFile.FullName, newFileFullPath);
     return newFileName;
   }
   catch (Exception e)
   {
     DispatcherHelper.CheckBeginInvokeOnUI(() =>
     {
       Messenger.Default.Send(new AddInfoInLogFileMessage()
       {
         MessageForLogFile = e.Message,
         TextColorInLog = "#000000"
       });
     });
   }
   return "errorExceptionCheckIt";
 }

Метод CheckEnteredPath(string pathToFolder) проверяет, правильный ли путь был передан на вход метода, выполняющего переименование.

private int CheckEnteredPath(string pathToFolder)
 {
   //Проверка наличия указанного пут����
   if (pathToFolder == "")
   {
     Messenger.Default.Send(new AddInfoInLogFileMessage() { MessageForLogFile = "Не задан путь до папки!", TextColorInLog = "#B22222" });
     return 0;
   }
   //Проверка существования выбранной папаки
   if (!System.IO.Directory.Exists(pathToFolder))
   {
     Messenger.Default.Send(new AddInfoInLogFileMessage() { MessageForLogFile = "Такой папки не существует!", TextColorInLog = "#B22222" });
     return 0;
   }
   return 1; 
 }

Чтобы получить список файлов находящихся в выбранной папке, мы используем метод

Directory.GetFiles(pathToFolder)

Который на выход получает путь до папки а возвращает имена файлов в ней.

Далее эти имена мы заносим в строго типизированный перечислитель:

IEnumerable<FileInfo> filesToRename = Directory.GetFiles(pathToFolder).Select(f => new FileInfo(f));

Тип FileInfo предоставляет свойства и методы для работы с файлами.

Если переименование проходит успешно, то мы отправляем сообщение из класса ViewModel в MainWindow.xaml.cs

Код во ViewModel

Messenger.Default.Send(new AddInfoInLogFileMessage()
  {
    MessageForLogFile = "Файл " + fileInfo.Name + " переименован в " + newFileName,
    TextColorInLog = "#000000"
 });

В конструкторе класса MainWindow.xaml.cs

Messenger.Default.Register<AddInfoInLogFileMessage>(this, x =>
{
  LogText_tb.AppendText(DateTime.Now.ToShortDateString() + " " + DateTime.Now.ToLongTimeString() + 
   " " + x.MessageForLogFile + "\n");
  LogText_tb.ScrollToEnd();
  ScrollViewerForLog.ScrollToBottom();
});

Чтобы отправлять сообщения мы создали вспомогательный класс AddInfoInLogFileMessage в папке Helpers. Он содержит в себе свойства, которые мы можем соотнести с параметрами, передаваемыми через сообщения.

class AddInfoInLogFileMessage
 {
   public string MessageForLogFile { get; set; }
   public string TextColorInLog { get; set; }
 }

Таким образом был описан алгоритм для добавления строки к именам всех файлов в выбранной папке. Теперь же покажу, как реализовать автоматическое отслеживание появления изменение структуры папки.

Автоматическое переименование файлов в папке.

UI содержит элемент CheckBox, который включает поток для отслеживания изменений в структуре папки (добавление, удаление, переименование файлов в ней)

<CheckBox Margin="0 10 0 10" IsChecked="{Binding AutoWatchingForFolder,
          UpdateSourceTrigger=PropertyChanged}">
  <TextBlock Text="Автоматически переименовывать файлы при появлении в папке" TextWrapping="Wrap"/>
    <i:Interaction.Triggers>
      <i:EventTrigger EventName="Checked">
         <i:InvokeCommandAction 
            Command="{Binding StartWatchingForFolder_Renaimer_Command}"/>
      </i:EventTrigger>
      <i:EventTrigger EventName="Unchecked">
          <i:InvokeCommandAction 
            Command="{Binding StopWatchingForFolder_Renaimer_Command}"/>
       </i:EventTrigger>
    </i:Interaction.Triggers> 
</CheckBox>

С помощью библиотеки System.Windows.Interactivity удаётся привязать события элемента UI к командам класса ViewModel.

//Начать отслеживать появление изменений в папке
public RelayCommand StartWatchingForFolder_Renaimer_Command
 { get
    {
      return _startWatchingForFolder_Ranaimer_Command ?? 
             (_startWatchingForFolder_Ranaimer_Command
              = new RelayCommand(() =>
              {
                //Проверка на наличие пути
                if (CheckEnteredPath(PathToFilesForAddString_Renamer) == 0)
                {
                  AutoWatchingForFolder = false;
                  return;
                }
                if (watcher == null)
                {
                  watcher = new FileSystemWatcher();
                  watcher.Path = PathToFilesForAddString_Renamer;
                  watcher.NotifyFilter = NotifyFilters.LastAccess
                              | NotifyFilters.FileName 
                              | NotifyFilters.DirectoryName;
                }
                watcher.Changed += (sender, args) =>
                {
                  //Отправка сообщения об изменении состояния файла
                  SendMessageInLogFromWatcher(args);
                };
                watcher.Created += (sender, args) =>
                {
                  //Отправка сообщения об изменении состояния файла
                  SendMessageInLogFromWatcher(args);
                  FileInfo inputFile = new FileInfo(args.FullPath);
                  Thread.Sleep(50);
                  if (RenameFileMethod_Renamer(inputFile) == "errorExceptionCheckIt")
                  { return; }
                };
                watcher.Deleted += (sender, args) =>
                {
                  //Отправка сообщения об изменении состояния файла
                  SendMessageInLogFromWatcher(args);
                };
                watcher.Renamed += (sender, args) =>
                {
                  //Отправка сообщения об изменении состояния файла
                  SendMessageInLogFromWatcher(args);
                };
                watcher.EnableRaisingEvents = true;
              }));
      }
    }
//Остановить отслеживание изменений в папке
public RelayCommand StopWatchingForFolder_Renaimer_Command
{
 get
   {
    return _stopWatchingForFolder_Ranaimer_Command ?? (_stopWatchingForFolder_Ranaimer_Command = new RelayCommand(() =>
      {
       if (watcher!=null)
        {
         watcher.EnableRaisingEvents = false;
         watcher = null;
        }
      }));
   }
}

В отслеживании помогает специальный класс FileSystemWatcher, который ожидает поступления уведомления от файловой системы об изменениях и инициализирует при этом события.

После создания экземпляра этого класса его нужно настроить

//Создание экземпляра класса
watcher = new FileSystemWatcher();
//Прописываем путь до папки
watcher.Path = PathToFilesForAddString_Renamer;
//Добавляем необходимые фильтры (В зависимости от них система будет выбирать на какие изменения ей нужно реагировать)
watcher.NotifyFilter = NotifyFilters.LastAccess
   | NotifyFilters.FileName 
   | NotifyFilters.DirectoryName;

Дальше нужно подписаться на события, которые возникают при изменениях в каталоге.

//Событие создания файла в каталоге. Самое нужное нам событие.
watcher.Created += (sender, args) =>
   {
//Отправка сообщения об изменении состояния файла
    SendMessageInLogFromWatcher(args);
    FileInfo inputFile = new FileInfo(args.FullPath);
    Thread.Sleep(50);
    if (RenameFileMethod_Renamer(inputFile) == "errorExceptionCheckIt")
     { return; }
    };

Так же здесь есть метод SendMessageInLogFromWatcher() который в качестве входного параметра получает экземпляр класса FileInfo и выводит в окно логов информацию о проведенной операции.

//Отправка сообщения об изменении состояния файла
public void SendMessageInLogFromWatcher(FileSystemEventArgs args)
{
  if (args is RenamedEventArgs)
  {
    DispatcherHelper.CheckBeginInvokeOnUI(() =>
    {
      Messenger.Default.Send(new AddInfoInLogFileMessage()
      {
        MessageForLogFile = "Файл: " + (args as RenamedEventArgs).OldName
                           + " был переименован в " + args.Name,
        TextColorInLog = "#000000"
      });
    });
    return;
  }
  DispatcherHelper.CheckBeginInvokeOnUI(() =>
  {
    Messenger.Default.Send(new AddInfoInLogFileMessage()
    {
      MessageForLogFile = "Файл: " + args.Name + " был " + args.ChangeType,
      TextColorInLog = "#000000"
    });
  });
}

Класс DispatcherHelper предоставляет возмодность отправлять информацию в виде сообщений между потоками.

Можно сказать, что реализация автоматического добавления строки к названию файла, который появляется в папке, завершена.

Удаление строки из названия файла.

Эта часть программы очень похожа на процесс добавления строк, поэтому описывать ее второй раз не вижу смысла. И так получился большой пост, перегруженный большим количеством кода. Как удалять строки можно посмотреть в исходном коде программы.

Итоги

Подведем маленький итог. Было создано простенькое приложение, которое изменяет имена всех файлов в выбранной папке. Удалось даже прикрутитьреализовать паттерн MVVM, такая архитектура пригодится в случае расширения возможностей программы.

Спасибо за внимание. По возникающим вопросам пишите в комментарии.

И, как обещал ссылка проекта на GitHub.