refactor mvvm, add datacontext

This commit is contained in:
2025-12-10 21:33:06 +05:00
parent b6ddadc6d3
commit 5479da163e
5 changed files with 566 additions and 621 deletions

View File

@@ -8,6 +8,10 @@
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Update="Properties\Settings.Designer.cs"> <Compile Update="Properties\Settings.Designer.cs">
<DesignTimeSharedInput>True</DesignTimeSharedInput> <DesignTimeSharedInput>True</DesignTimeSharedInput>

View File

@@ -1,77 +1,66 @@
using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel;
using System.ComponentModel; using System.Collections.ObjectModel;
using System.Runtime.CompilerServices;
namespace CodeContextGenerator.Models namespace CodeContextGenerator.Models
{ {
public class FileItem : INotifyPropertyChanged public partial class FileItem : ObservableObject
{ {
public string Name { get; set; } [ObservableProperty]
public string FullName { get; set; } private string? name;
public bool IsDirectory { get; set; }
public ObservableCollection<FileItem> Children { get; set; } = new ObservableCollection<FileItem>();
public FileItem Parent { get; set; }
private bool? _isSelected; [ObservableProperty]
public bool? IsSelected private string? fullName;
[ObservableProperty]
private bool isDirectory;
[ObservableProperty]
private bool? isSelected;
[ObservableProperty]
private FileItem? parent;
public ObservableCollection<FileItem> Children { get; } = new ObservableCollection<FileItem>();
/*partial void OnIsSelectedChanged(bool? oldValue, bool? newValue)
{ {
get => _isSelected; if (newValue.HasValue)
set
{ {
if (_isSelected != value) UpdateChildrenSelection(newValue.Value);
{
_isSelected = value;
OnPropertyChanged();
UpdateParentSelection();
// Если это директория и установлено конкретное значение (не null), применяем ко всем детям
if (IsDirectory && value.HasValue)
{
foreach (var child in Children)
{
child.IsSelected = value.Value;
}
}
}
} }
} UpdateParentSelection();
}*/
public event PropertyChangedEventHandler? PropertyChanged; private void UpdateChildrenSelection(bool value)
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); if (!IsDirectory) return;
foreach (var child in Children)
{
child.IsSelected = value;
}
} }
private void UpdateParentSelection() private void UpdateParentSelection()
{ {
if (Parent == null) return; if (Parent == null) return;
var children = Parent.Children.ToList(); var children = Parent.Children;
var allSelected = children.All(c => c.IsSelected == true); var allSelected = children.All(c => c.IsSelected == true);
var noneSelected = children.All(c => c.IsSelected == false); var noneSelected = children.All(c => c.IsSelected == false);
var hasIndeterminate = children.Any(c => c.IsSelected == null);
// Если есть дети с null - устанавливаем null Parent.IsSelected = hasIndeterminate ? null : (allSelected ? true : (noneSelected ? false : null));
bool hasIndeterminate = children.Any(c => c.IsSelected == null);
if (hasIndeterminate)
{
Parent.IsSelected = null;
}
else if (allSelected)
{
Parent.IsSelected = true;
}
else if (noneSelected)
{
Parent.IsSelected = false;
}
else
{
Parent.IsSelected = null;
}
Parent.UpdateParentSelection(); Parent.UpdateParentSelection();
} }
public void ClearSelection()
{
IsSelected = false;
foreach (var child in Children)
{
child.ClearSelection();
}
}
} }
} }

View File

@@ -1,522 +1,431 @@
using System.ComponentModel; using CodeContextGenerator.Models;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;
using Microsoft.Win32;
using CodeContextGenerator.Models;
using CodeContextGenerator.Services; using CodeContextGenerator.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Win32;
using System.ComponentModel;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Windows;
using System.Threading;
using System.Collections.Generic;
using System.Linq;
namespace CodeContextGenerator.ViewModels namespace CodeContextGenerator.ViewModels;
public partial class MainViewModel : ObservableObject, INotifyPropertyChanged
{ {
public class MainViewModel : INotifyPropertyChanged [ObservableProperty]
private FileItem? _rootDirectory = new();
[ObservableProperty]
private string _selectedProjectFilePath = string.Empty;
[ObservableProperty]
private bool _isProcessing = false;
[ObservableProperty]
private int _progressValue;
[ObservableProperty]
private string _progressText = string.Empty;
[ObservableProperty]
private string? _lastProjectPath = string.Empty;
[ObservableProperty]
private bool _isProjectLoaded;
private CancellationTokenSource _cancellationTokenSource = new();
public bool CanSelectFolder => !IsProcessing;
public bool CanGenerate => !IsProcessing && IsProjectLoaded && HasSelectedFiles(RootDirectory);
//public ICommand SelectProjectFileCommand { get; }
//public ICommand GenerateCommand { get; }
//public ICommand CancelCommand { get; }
//public ICommand ExitCommand { get; }
public MainViewModel()
{ {
private FileItem _rootDirectory; //ExitCommand = new RelayCommand(_ => Application.Current.Shutdown());
private string _selectedProjectFilePath; LoadSettings();
private bool _isProcessing; }
private int _progressValue;
private string _progressText;
private CancellationTokenSource _cancellationTokenSource;
private string _lastProjectPath;
private bool _isProjectLoaded;
public FileItem RootDirectory private bool HasSelectedFiles(FileItem item)
{
if (item == null) return false;
if (item.IsSelected == true && !item.IsDirectory) return true;
return item.Children.Any(HasSelectedFiles);
}
//[RelayCommand(CanExecute = nameof(CanSelectFolder))]
[RelayCommand(CanExecute = nameof(CanSelectFolder))]
private void Exit()
{
Application.Current.Shutdown();
}
//[RelayCommand(CanExecute = nameof(CanSelectFolder))]
[RelayCommand(CanExecute = nameof(CanGenerate))]
private void Cancel()
{
CancelProcessing();
}
[RelayCommand(CanExecute = nameof(CanSelectFolder))]
private async Task SelectProjectFileAsync()
{
var dialog = new OpenFileDialog
{ {
get => _rootDirectory; Title = "Выберите файл решения или проекта",
set CheckFileExists = true,
CheckPathExists = true,
Filter = "Файлы решений (*.sln)|*.sln|Файлы проектов (*.csproj)|*.csproj|Все поддерживаемые (*.sln;*.csproj)|*.sln;*.csproj|Все файлы (*.*)|*.*",
InitialDirectory = !string.IsNullOrEmpty(LastProjectPath) && Directory.Exists(LastProjectPath) ? LastProjectPath : null
};
if (dialog.ShowDialog() != true) return;
SelectedProjectFilePath = dialog.FileName;
LastProjectPath = Path.GetDirectoryName(SelectedProjectFilePath);
SaveSettings();
IsProjectLoaded = false;
await LoadProjectAsync(SelectedProjectFilePath);
}
private async Task LoadProjectAsync(string projectFilePath)
{
IsProcessing = true;
ProgressText = "Загрузка проекта...";
ProgressValue = 0;
try
{
string? projectDirectory = Path.GetDirectoryName(projectFilePath);
var progress = new Progress<int>(value =>
{ {
_rootDirectory = value; ProgressValue = value;
OnPropertyChanged(); ProgressText = $"Загрузка: {value}%";
});
_cancellationTokenSource = new CancellationTokenSource();
if (projectFilePath.EndsWith(".sln", StringComparison.OrdinalIgnoreCase))
{
await LoadSolutionAsync(projectFilePath, progress, _cancellationTokenSource.Token);
} }
} else if (projectFilePath.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase))
public string SelectedProjectFilePath
{
get => _selectedProjectFilePath;
set
{ {
_selectedProjectFilePath = value; await LoadProjectDirectoryAsync(projectDirectory, progress, _cancellationTokenSource.Token);
OnPropertyChanged();
}
}
public bool IsProcessing
{
get => _isProcessing;
set
{
_isProcessing = value;
OnPropertyChanged();
OnPropertyChanged(nameof(CanSelectFolder));
OnPropertyChanged(nameof(CanGenerate));
}
}
public int ProgressValue
{
get => _progressValue;
set
{
_progressValue = value;
OnPropertyChanged();
}
}
public string ProgressText
{
get => _progressText;
set
{
_progressText = value;
OnPropertyChanged();
}
}
public bool IsProjectLoaded
{
get => _isProjectLoaded;
set
{
_isProjectLoaded = value;
OnPropertyChanged();
}
}
public bool CanSelectFolder => !IsProcessing;
public bool CanGenerate => !IsProcessing && IsProjectLoaded && HasSelectedFiles(RootDirectory);
public ICommand SelectProjectFileCommand { get; }
public ICommand GenerateCommand { get; }
public ICommand CancelCommand { get; }
public ICommand ExitCommand { get; }
public MainViewModel()
{
SelectProjectFileCommand = new RelayCommand(SelectProjectFileAsync);
GenerateCommand = new RelayCommand(GenerateFileAsync, _ => CanGenerate);
CancelCommand = new RelayCommand(CancelProcessing, _ => IsProcessing);
ExitCommand = new RelayCommand(_ => Application.Current.Shutdown());
LoadSettings();
}
private bool HasSelectedFiles(FileItem item)
{
if (item == null) return false;
if (item.IsSelected == true && !item.IsDirectory)
return true;
foreach (var child in item.Children)
{
if (HasSelectedFiles(child))
return true;
} }
return false; IsProjectLoaded = true;
ProgressText = "Проект загружен успешно";
} }
catch (OperationCanceledException)
private async void SelectProjectFileAsync(object parameter)
{ {
if (!CanSelectFolder) return; ProgressText = "Загрузка отменена";
RootDirectory = null;
IsProjectLoaded = false;
}
catch (Exception ex)
{
MessageBox.Show($"Ошибка при загрузке проекта: {ex.Message}", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
RootDirectory = null;
IsProjectLoaded = false;
}
finally
{
IsProcessing = false;
}
}
var dialog = new OpenFileDialog private async Task LoadSolutionAsync(string solutionPath, IProgress<int> progress, CancellationToken cancellationToken)
{
try
{
string? solutionDirectory = Path.GetDirectoryName(solutionPath);
var solutionProjects = ParseSolutionProjects(solutionPath);
var solutionItem = new FileItem
{ {
Title = "Выберите файл решения или проекта", Name = Path.GetFileName(solutionPath),
CheckFileExists = true, FullName = solutionPath,
CheckPathExists = true, IsDirectory = true,
Filter = "Файлы решений Visual Studio (*.sln)|*.sln|Файлы проектов C# (*.csproj)|*.csproj|Все поддерживаемые файлы (*.sln;*.csproj)|*.sln;*.csproj|Все файлы (*.*)|*.*", IsSelected = false
Multiselect = false
}; };
if (!string.IsNullOrEmpty(_lastProjectPath) && Directory.Exists(_lastProjectPath)) int totalProjects = solutionProjects.Count;
{ int processedProjects = 0;
dialog.InitialDirectory = _lastProjectPath;
}
bool? result = dialog.ShowDialog(); foreach (var projectPath in solutionProjects)
if (result == true)
{
string selectedFilePath = dialog.FileName;
string projectDirectory = Path.GetDirectoryName(selectedFilePath);
if (!string.IsNullOrEmpty(projectDirectory))
{
_lastProjectPath = projectDirectory;
SaveSettings();
SelectedProjectFilePath = selectedFilePath;
IsProjectLoaded = false;
await LoadProjectAsync(selectedFilePath);
}
}
}
private async Task LoadProjectAsync(string projectFilePath)
{
IsProcessing = true;
ProgressText = "Загрузка проекта...";
ProgressValue = 0;
try
{
string projectDirectory = Path.GetDirectoryName(projectFilePath);
SelectedProjectFilePath = projectFilePath;
var progress = new Progress<int>(value =>
{
ProgressValue = value;
ProgressText = $"Загрузка: {value}%";
});
_cancellationTokenSource = new CancellationTokenSource();
// Определяем тип файла
if (projectFilePath.EndsWith(".sln", StringComparison.OrdinalIgnoreCase))
{
await LoadSolutionAsync(projectFilePath, progress, _cancellationTokenSource.Token);
}
else if (projectFilePath.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase))
{
await LoadProjectDirectoryAsync(projectDirectory, progress, _cancellationTokenSource.Token);
}
IsProjectLoaded = true;
ProgressText = "Проект загружен успешно";
}
catch (OperationCanceledException)
{
ProgressText = "Загрузка отменена";
RootDirectory = null;
IsProjectLoaded = false;
}
catch (Exception ex)
{
MessageBox.Show($"Ошибка при загрузке проекта: {ex.Message}", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
RootDirectory = null;
IsProjectLoaded = false;
}
finally
{
IsProcessing = false;
}
}
private async Task LoadSolutionAsync(string solutionPath, IProgress<int> progress, CancellationToken cancellationToken)
{
try
{
string solutionDirectory = Path.GetDirectoryName(solutionPath);
var solutionProjects = ParseSolutionProjects(solutionPath);
var solutionItem = new FileItem
{
Name = Path.GetFileName(solutionPath),
FullName = solutionPath,
IsDirectory = true,
IsSelected = false
};
int totalProjects = solutionProjects.Count;
int processedProjects = 0;
foreach (var projectPath in solutionProjects)
{
cancellationToken.ThrowIfCancellationRequested();
if (File.Exists(projectPath))
{
string projectDir = Path.GetDirectoryName(projectPath);
string projectName = Path.GetFileName(projectDir);
var projectItem = new FileItem
{
Name = projectName,
FullName = projectDir,
IsDirectory = true,
Parent = solutionItem,
IsSelected = false
};
await ProjectScannerService.BuildDirectoryTreeAsync(projectDir, projectItem, progress, cancellationToken);
if (projectItem.Children.Any())
{
solutionItem.Children.Add(projectItem);
}
}
processedProjects++;
progress?.Report((int)((processedProjects * 100.0) / totalProjects));
}
RootDirectory = solutionItem;
// После загрузки решения - не выбираем ничего по умолчанию
ClearAllSelections(solutionItem);
}
catch (Exception ex)
{
throw new Exception($"Ошибка при обработке решения: {ex.Message}", ex);
}
}
private List<string> ParseSolutionProjects(string solutionPath)
{
var projects = new List<string>();
try
{
string solutionDirectory = Path.GetDirectoryName(solutionPath);
foreach (string line in File.ReadAllLines(solutionPath))
{
if (line.Trim().StartsWith("Project(", StringComparison.OrdinalIgnoreCase))
{
var parts = line.Split(new[] { '"' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 3)
{
string relativePath = parts[2].Trim();
string absolutePath = Path.GetFullPath(Path.Combine(solutionDirectory, relativePath));
if (absolutePath.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase) && File.Exists(absolutePath))
{
projects.Add(absolutePath);
}
}
}
}
}
catch (Exception ex)
{
throw new Exception($"Ошибка парсинга файла решения: {ex.Message}", ex);
}
return projects;
}
private async Task LoadProjectDirectoryAsync(string projectDirectory, IProgress<int> progress, CancellationToken cancellationToken)
{
try
{
var projectName = Path.GetFileName(projectDirectory);
var rootItem = new FileItem
{
Name = projectName,
FullName = projectDirectory,
IsDirectory = true,
IsSelected = false
};
await ProjectScannerService.BuildDirectoryTreeAsync(projectDirectory, rootItem, progress, cancellationToken);
RootDirectory = rootItem;
// После загрузки проекта - не выбираем ничего по умолчанию
ClearAllSelections(rootItem);
}
catch (Exception ex)
{
throw new Exception($"Ошибка при загрузке проекта: {ex.Message}", ex);
}
}
private void ClearAllSelections(FileItem item)
{
item.IsSelected = false;
foreach (var child in item.Children)
{
ClearAllSelections(child);
}
}
private async void GenerateFileAsync(object parameter)
{
if (!CanGenerate || RootDirectory == null) return;
var selectedFiles = GetSelectedFiles(RootDirectory);
if (selectedFiles.Count == 0)
{
MessageBox.Show("Пожалуйста, выберите хотя бы один файл для обработки.", "Предупреждение", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var saveDialog = new SaveFileDialog
{
Title = "Сохранить контекстный файл",
Filter = "Текстовые файлы (*.txt)|*.txt|Все файлы (*.*)|*.*",
FileName = $"{Path.GetFileNameWithoutExtension(SelectedProjectFilePath)}_context.txt",
DefaultExt = ".txt"
};
if (!string.IsNullOrEmpty(_lastProjectPath) && Directory.Exists(_lastProjectPath))
{
saveDialog.InitialDirectory = _lastProjectPath;
}
bool? result = saveDialog.ShowDialog();
if (result == true)
{
IsProcessing = true;
ProgressText = "Генерация контекстного файла...";
ProgressValue = 0;
try
{
_cancellationTokenSource = new CancellationTokenSource();
var progress = new Progress<int>(value =>
{
ProgressValue = value;
ProgressText = $"Генерация: {value}%";
});
await GenerateContextFileAsync(selectedFiles, saveDialog.FileName, progress, _cancellationTokenSource.Token);
MessageBox.Show($"Файл успешно создан:\n{saveDialog.FileName}", "Успех", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (OperationCanceledException)
{
ProgressText = "Генерация отменена";
}
catch (IOException ex)
{
MessageBox.Show($"Ошибка доступа к файлу: {ex.Message}\nПожалуйста, закройте файлы или предоставьте необходимые права доступа.", "Ошибка доступа", MessageBoxButton.OK, MessageBoxImage.Error);
}
catch (Exception ex)
{
MessageBox.Show($"Ошибка при генерации файла: {ex.Message}", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
IsProcessing = false;
}
}
}
private List<string> GetSelectedFiles(FileItem rootItem)
{
var selectedFiles = new List<string>();
CollectSelectedFiles(rootItem, selectedFiles);
return selectedFiles;
}
private void CollectSelectedFiles(FileItem item, List<string> selectedFiles)
{
if (item.IsDirectory)
{
foreach (var child in item.Children)
{
CollectSelectedFiles(child, selectedFiles);
}
}
else if (item.IsSelected == true)
{
selectedFiles.Add(item.FullName);
}
}
private async Task GenerateContextFileAsync(List<string> selectedFiles, string outputPath, IProgress<int> progress, CancellationToken cancellationToken)
{
var outputContent = new StringBuilder();
int totalFiles = selectedFiles.Count;
int processedFiles = 0;
foreach (var filePath in selectedFiles)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
try if (File.Exists(projectPath))
{ {
string projectDir = Path.GetDirectoryName(SelectedProjectFilePath); string? projectDir = Path.GetDirectoryName(projectPath);
string relativePath = Path.GetRelativePath(projectDir, filePath); string? projectName = Path.GetFileName(projectDir);
string fileContent = await File.ReadAllTextAsync(filePath, Encoding.UTF8);
// Обработка комментариев var projectItem = new FileItem
fileContent = FileProcessorService.ProcessFileContent(fileContent, Path.GetFileName(filePath)); {
fileContent = FileProcessorService.RemoveMultiLineComments(fileContent); Name = projectName,
FullName = projectDir,
IsDirectory = true,
Parent = solutionItem,
IsSelected = false
};
outputContent.AppendLine($"=== Файл: {relativePath} ==="); await ProjectScannerService.BuildDirectoryTreeAsync(projectDir, projectItem, progress, cancellationToken);
outputContent.AppendLine(fileContent);
outputContent.AppendLine(); // Пустая строка между файлами
processedFiles++; if (projectItem.Children.Any())
int progressValue = (int)((processedFiles * 100.0) / totalFiles); {
progress?.Report(progressValue); solutionItem.Children.Add(projectItem);
} }
catch (IOException ex)
{
throw new IOException($"Файл '{Path.GetFileName(filePath)}' заблокирован или недоступен: {ex.Message}", ex);
} }
processedProjects++;
progress?.Report((int)((processedProjects * 100.0) / totalProjects));
} }
// Сохраняем результат с кодировкой UTF-8 с BOM RootDirectory = solutionItem;
var encoding = new UTF8Encoding(true);
await File.WriteAllTextAsync(outputPath, outputContent.ToString(), encoding); // После загрузки решения - не выбираем ничего по умолчанию
ClearAllSelections(solutionItem);
} }
catch (Exception ex)
private void CancelProcessing(object parameter)
{ {
_cancellationTokenSource?.Cancel(); throw new Exception($"Ошибка при обработке решения: {ex.Message}", ex);
}
private void LoadSettings()
{
try
{
_lastProjectPath = Properties.Settings.Default.LastProjectPath;
}
catch
{
_lastProjectPath = null;
}
}
private void SaveSettings()
{
try
{
Properties.Settings.Default.LastProjectPath = _lastProjectPath;
Properties.Settings.Default.Save();
}
catch
{
// Игнорируем ошибки сохранения настроек
}
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }
} }
public class RelayCommand : ICommand private static List<string> ParseSolutionProjects(string solutionPath)
{ {
private readonly Action<object> _execute; var projects = new List<string>();
private readonly Func<object, bool> _canExecute; try
public event EventHandler? CanExecuteChanged
{ {
add => CommandManager.RequerySuggested += value; string? solutionDirectory = Path.GetDirectoryName(solutionPath);
remove => CommandManager.RequerySuggested -= value;
}
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null) foreach (string line in File.ReadAllLines(solutionPath))
{
if (line.Trim().StartsWith("Project(", StringComparison.OrdinalIgnoreCase))
{
var parts = line.Split(['"'], StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 3)
{
string relativePath = parts[2].Trim();
string absolutePath = Path.GetFullPath(Path.Combine(solutionDirectory, relativePath));
if (absolutePath.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase) && File.Exists(absolutePath))
{
projects.Add(absolutePath);
}
}
}
}
}
catch (Exception ex)
{ {
_execute = execute ?? throw new ArgumentNullException(nameof(execute)); throw new Exception($"Ошибка парсинга файла решения: {ex.Message}", ex);
_canExecute = canExecute;
} }
return projects;
public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter);
public void Execute(object parameter) => _execute(parameter);
} }
}
private async Task LoadProjectDirectoryAsync(string projectDirectory, IProgress<int> progress, CancellationToken cancellationToken)
{
try
{
var projectName = Path.GetFileName(projectDirectory);
var rootItem = new FileItem
{
Name = projectName,
FullName = projectDirectory,
IsDirectory = true,
IsSelected = false
};
await ProjectScannerService.BuildDirectoryTreeAsync(projectDirectory, rootItem, progress, cancellationToken);
RootDirectory = rootItem;
// После загрузки проекта - не выбираем ничего по умолчанию
ClearAllSelections(rootItem);
}
catch (Exception ex)
{
throw new Exception($"Ошибка при загрузке проекта: {ex.Message}", ex);
}
}
private static void ClearAllSelections(FileItem item)
{
item.IsSelected = false;
foreach (var child in item.Children)
{
ClearAllSelections(child);
}
}
[RelayCommand(CanExecute = nameof(CanGenerate))]
private async Task GenerateFileAsync()
{
var selectedFiles = GetSelectedFiles(RootDirectory);
if (selectedFiles.Count == 0)
{
MessageBox.Show("Пожалуйста, выберите хотя бы один файл для обработки.", "Предупреждение", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var saveDialog = new SaveFileDialog
{
Title = "Сохранить контекстный файл",
Filter = "Текстовые файлы (*.txt)|*.txt|Все файлы (*.*)|*.*",
FileName = $"{Path.GetFileNameWithoutExtension(SelectedProjectFilePath)}_context.txt",
DefaultExt = ".txt",
InitialDirectory = LastProjectPath
};
if (saveDialog.ShowDialog() != true) return;
IsProcessing = true;
try
{
_cancellationTokenSource = new CancellationTokenSource();
var progress = new Progress<int>(value =>
{
ProgressValue = value;
ProgressText = $"Генерация: {value}%";
});
await GenerateContextFileAsync(selectedFiles, saveDialog.FileName, progress, _cancellationTokenSource.Token);
MessageBox.Show($"Файл успешно создан:\n{saveDialog.FileName}", "Успех", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (OperationCanceledException)
{
ProgressText = "Генерация отменена";
}
catch (IOException ex)
{
MessageBox.Show($"Ошибка доступа к файлу: {ex.Message}", "Ошибка доступа", MessageBoxButton.OK, MessageBoxImage.Error);
}
catch (Exception ex)
{
MessageBox.Show($"Ошибка при генерации файла: {ex.Message}", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
IsProcessing = false;
}
}
private List<string> GetSelectedFiles(FileItem rootItem)
{
var selectedFiles = new List<string>();
CollectSelectedFiles(rootItem, selectedFiles);
return selectedFiles;
}
private static void CollectSelectedFiles(FileItem item, List<string> selectedFiles)
{
if (item.IsDirectory)
{
foreach (var child in item.Children)
{
CollectSelectedFiles(child, selectedFiles);
}
}
else if (item.IsSelected == true)
{
selectedFiles.Add(item.FullName);
}
}
private async Task GenerateContextFileAsync(List<string> selectedFiles, string outputPath, IProgress<int> progress, CancellationToken cancellationToken)
{
var outputContent = new StringBuilder();
int totalFiles = selectedFiles.Count;
int processedFiles = 0;
foreach (var filePath in selectedFiles)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
string? projectDir = Path.GetDirectoryName(SelectedProjectFilePath);
string? relativePath = Path.GetRelativePath(projectDir, filePath);
string? fileContent = await File.ReadAllTextAsync(filePath, Encoding.UTF8);
// Обработка комментариев
fileContent = FileProcessorService.ProcessFileContent(fileContent, Path.GetFileName(filePath));
fileContent = FileProcessorService.RemoveMultiLineComments(fileContent);
outputContent.AppendLine($"=== Файл: {relativePath} ===");
outputContent.AppendLine(fileContent);
outputContent.AppendLine(); // Пустая строка между файлами
processedFiles++;
int progressValue = (int)((processedFiles * 100.0) / totalFiles);
progress?.Report(progressValue);
}
catch (IOException ex)
{
throw new IOException($"Файл '{Path.GetFileName(filePath)}' заблокирован или недоступен: {ex.Message}", ex);
}
}
// Сохраняем результат с кодировкой UTF-8 с BOM
var encoding = new UTF8Encoding(true);
await File.WriteAllTextAsync(outputPath, outputContent.ToString(), encoding);
}
[RelayCommand]
private void CancelProcessing()
{
_cancellationTokenSource?.Cancel();
}
private void LoadSettings()
{
try
{
LastProjectPath = Properties.Settings.Default.LastProjectPath;
}
catch
{
LastProjectPath = string.Empty;
}
}
private void SaveSettings()
{
try
{
Properties.Settings.Default.LastProjectPath = LastProjectPath;
Properties.Settings.Default.Save();
}
catch { }
}
/*public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}*/
}
/*
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly 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)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter);
public void Execute(object parameter) => _execute(parameter);
}*/

View File

@@ -1,68 +1,95 @@
<Window x:Class="CodeContextGenerator.Views.MainWindow" <Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" x:Class="CodeContextGenerator.Views.MainWindow"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 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:local="clr-namespace:CodeContextGenerator.Views" xmlns:local="clr-namespace:CodeContextGenerator.Views"
mc:Ignorable="d" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="Code Context Generator" Height="600" Width="800" xmlns:viewmodels="clr-namespace:CodeContextGenerator.ViewModels"
WindowStartupLocation="CenterScreen"> Title="Code Context Generator"
Width="800"
Height="600"
d:DataContext="{d:DesignInstance Type=viewmodels:MainViewModel}"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Window.Resources> <Window.Resources>
<Style TargetType="TreeViewItem"> <Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="True"/> <Setter Property="IsExpanded" Value="True" />
<Setter Property="Focusable" Value="False"/> <Setter Property="Focusable" Value="False" />
</Style> </Style>
<Style TargetType="Button"> <Style TargetType="Button">
<Setter Property="Padding" Value="10,5"/> <Setter Property="Padding" Value="10,5" />
<Setter Property="Margin" Value="5,0"/> <Setter Property="Margin" Value="5,0" />
</Style> </Style>
<Style TargetType="TextBlock"> <Style TargetType="TextBlock">
<Setter Property="VerticalAlignment" Value="Center"/> <Setter Property="VerticalAlignment" Value="Center" />
</Style> </Style>
</Window.Resources> </Window.Resources>
<Grid Margin="10"> <Grid Margin="10">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto" />
<RowDefinition Height="*"/> <RowDefinition Height="*" />
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Выберите файл решения (.sln) или проекта (.csproj):" FontWeight="Bold" Margin="0,0,0,5"/> <TextBlock
Grid.Row="0"
Margin="0,0,0,5"
FontWeight="Bold"
Text="Выберите файл решения (.sln) или проекта (.csproj):" />
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,0,0,10"> <StackPanel
<Button Content="Выбрать файл..." Grid.Row="1"
Command="{Binding SelectProjectFileCommand}" Margin="0,0,0,10"
IsEnabled="{Binding CanSelectFolder}" Orientation="Horizontal">
Width="120"/> <Button
<TextBlock Text="{Binding SelectedProjectFilePath}" Width="120"
VerticalAlignment="Center" Command="{Binding SelectProjectFileCommand}"
TextWrapping="Wrap" Content="Выбрать файл..."
MaxWidth="600" IsEnabled="{Binding CanSelectFolder}" />
Margin="10,0,0,0"/> <TextBlock
MaxWidth="600"
Margin="10,0,0,0"
VerticalAlignment="Center"
Text="{Binding SelectedProjectFilePath}"
TextWrapping="Wrap" />
</StackPanel> </StackPanel>
<Border Grid.Row="2" BorderBrush="Gray" BorderThickness="1" CornerRadius="4" Padding="5" <Border
Visibility="{Binding IsProjectLoaded, Converter={StaticResource BooleanToVisibilityConverter}}"> Grid.Row="2"
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Padding="5"
PreviewMouseWheel="ScrollViewer_PreviewMouseWheel"> BorderBrush="Gray"
<TreeView x:Name="ProjectTree" ItemsSource="{Binding RootDirectory.Children}" BorderThickness="1"
VirtualizingPanel.IsVirtualizing="True" CornerRadius="4"
VirtualizingPanel.VirtualizationMode="Recycling"> Visibility="{Binding IsProjectLoaded, Converter={StaticResource BooleanToVisibilityConverter}}">
<ScrollViewer
HorizontalScrollBarVisibility="Auto"
PreviewMouseWheel="ScrollViewer_PreviewMouseWheel"
VerticalScrollBarVisibility="Auto">
<TreeView
x:Name="ProjectTree"
ItemsSource="{Binding RootDirectory.Children}"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling">
<TreeView.ItemTemplate> <TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}"> <HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" MinHeight="24"> <StackPanel MinHeight="24" Orientation="Horizontal">
<CheckBox IsChecked="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged}" <CheckBox
IsThreeState="True" Margin="2,0,5,0"
VerticalAlignment="Center" Margin="2,0,5,0" VerticalAlignment="Center"
Click="CheckBox_Click"/> Click="CheckBox_Click"
<TextBlock Text="{Binding Name}" VerticalAlignment="Center" IsChecked="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged}"
ToolTip="{Binding FullName}"/> IsThreeState="True" />
<TextBlock
VerticalAlignment="Center"
Text="{Binding Name}"
ToolTip="{Binding FullName}" />
</StackPanel> </StackPanel>
</HierarchicalDataTemplate> </HierarchicalDataTemplate>
</TreeView.ItemTemplate> </TreeView.ItemTemplate>
@@ -70,27 +97,49 @@
</ScrollViewer> </ScrollViewer>
</Border> </Border>
<TextBlock Grid.Row="2" Text="Проект еще не загружен. Выберите файл решения или проекта." <TextBlock
HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Row="2"
Foreground="Gray" FontSize="14" HorizontalAlignment="Center"
Visibility="{Binding IsProjectLoaded, Converter={StaticResource BooleanToVisibilityConverter}, ConverterParameter=Collapsed}"/> VerticalAlignment="Center"
FontSize="14"
Foreground="Gray"
Text="Проект еще не загружен. Выберите файл решения или проекта."
Visibility="{Binding IsProjectLoaded, Converter={StaticResource BooleanToVisibilityConverter}, ConverterParameter=Collapsed}" />
<ProgressBar Grid.Row="3" Value="{Binding ProgressValue}" Height="20" Margin="0,10,0,10" <ProgressBar
Visibility="{Binding IsProcessing, Converter={StaticResource BooleanToVisibilityConverter}}"/> Grid.Row="3"
Height="20"
Margin="0,10,0,10"
Visibility="{Binding IsProcessing, Converter={StaticResource BooleanToVisibilityConverter}}"
Value="{Binding ProgressValue}" />
<TextBlock Grid.Row="3" Text="{Binding ProgressText}" HorizontalAlignment="Center" <TextBlock
VerticalAlignment="Center" FontWeight="Bold" Grid.Row="3"
Visibility="{Binding IsProcessing, Converter={StaticResource BooleanToVisibilityConverter}}"/> HorizontalAlignment="Center"
VerticalAlignment="Center"
FontWeight="Bold"
Text="{Binding ProgressText}"
Visibility="{Binding IsProcessing, Converter={StaticResource BooleanToVisibilityConverter}}" />
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0"> <StackPanel
<Button Content="Отмена" Command="{Binding CancelCommand}" Grid.Row="4"
Visibility="{Binding IsProcessing, Converter={StaticResource BooleanToVisibilityConverter}}" Margin="0,10,0,0"
Background="#FFDC3545" Foreground="White"/> HorizontalAlignment="Right"
<Button Content="Закрыть" Command="{Binding ExitCommand}"/> Orientation="Horizontal">
<Button Content="Сформировать" Command="{Binding GenerateCommand}" <Button
IsEnabled="{Binding CanGenerate}" Background="#FFDC3545"
Background="#FF28A745" Foreground="White" Command="{Binding CancelCommand}"
Visibility="{Binding IsProjectLoaded, Converter={StaticResource BooleanToVisibilityConverter}}"/> Content="Отмена"
Foreground="White"
Visibility="{Binding IsProcessing, Converter={StaticResource BooleanToVisibilityConverter}}" />
<Button Command="{Binding ExitCommand}" Content="Закрыть" />
<Button
Background="#FF28A745"
Command="{Binding GenerateFileCommand}"
Content="Сформировать"
Foreground="White"
IsEnabled="{Binding CanGenerate}"
Visibility="{Binding IsProjectLoaded, Converter={StaticResource BooleanToVisibilityConverter}}" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</Window> </Window>

View File

@@ -4,37 +4,31 @@ using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
namespace CodeContextGenerator.Views namespace CodeContextGenerator.Views;
public partial class MainWindow : Window
{ {
public partial class MainWindow : Window public MainWindow()
{ {
public MainWindow() InitializeComponent();
{ DataContext = new MainViewModel();
InitializeComponent(); }
}
private void ScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e) private void ScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (sender is ScrollViewer scrollViewer)
{ {
if (sender is ScrollViewer scrollViewer) scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - e.Delta / 3);
{ e.Handled = true;
scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - e.Delta / 3);
e.Handled = true;
}
} }
}
private void CheckBox_Click(object sender, RoutedEventArgs e) private void CheckBox_Click(object sender, RoutedEventArgs e)
{
if (sender is CheckBox checkBox && checkBox.DataContext is FileItem fileItem)
{ {
if (sender is CheckBox checkBox && checkBox.DataContext is FileItem fileItem) // Синхронизация состояния чекбокса с ViewModel
{ fileItem.IsSelected = checkBox.IsChecked;
// Принудительно обновляем состояние детей при клике
if (fileItem.IsDirectory && checkBox.IsChecked.HasValue)
{
foreach (var child in fileItem.Children)
{
child.IsSelected = checkBox.IsChecked;
}
}
}
} }
} }
} }