Продолжение рефакторинга (вынесение WPF в отдельный проект)

This commit is contained in:
2025-10-02 17:25:48 +04:00
parent 2fa94851cb
commit f5b311e025
161 changed files with 899 additions and 559 deletions
+10
View File
@@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
@@ -0,0 +1,106 @@
<UserControl
x:Class="Pf2e.WPF.EntityViewer.Component.EntityListControl"
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"
xmlns:viewmodel="clr-namespace:Pf2e.WPF.EntityViewer.ViewModel"
Padding="3,3,3,3"
d:DataContext="{d:DesignInstance Type=viewmodel:EntityListViewModel}"
d:DesignHeight="450"
d:DesignWidth="400"
x:ClassModifier="internal"
mc:Ignorable="d">
<UserControl.Background>
<SolidColorBrush Color="{DynamicResource {x:Static SystemColors.ControlColorKey}}" />
</UserControl.Background>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="25" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<DockPanel Grid.Row="0" Grid.Column="0">
<Button
Width="25"
Margin="0,0,3,0"
Command="{Binding FirstPageCommand}"
Content="&lt;&lt;"
DockPanel.Dock="Left"
IsEnabled="{Binding HasPrevious}" />
<Button
Width="25"
Margin="0,0,3,0"
Command="{Binding PreviousPageCommand}"
Content="&lt;"
DockPanel.Dock="Left"
IsEnabled="{Binding HasPrevious}" />
<Button
Width="25"
Margin="3,0,0,0"
Command="{Binding LastPageCommand}"
Content="&gt;&gt;"
DockPanel.Dock="Right"
IsEnabled="{Binding HasNext}" />
<Button
Width="25"
Margin="3,0,0,0"
Command="{Binding NextPageCommand}"
Content="&gt;"
DockPanel.Dock="Right"
IsEnabled="{Binding HasNext}" />
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<Label VerticalContentAlignment="Center" Content="Страница" />
<TextBox
Width="30"
VerticalContentAlignment="Center"
PreviewTextInput="TextBox_PreviewTextInput">
<TextBox.Text>
<Binding
Mode="TwoWay"
Path="Page"
UpdateSourceTrigger="PropertyChanged" />
</TextBox.Text>
</TextBox>
<Label VerticalContentAlignment="Center" Content="из" />
<Label VerticalContentAlignment="Center" Content="{Binding PageCount, Mode=OneWay}" />
</StackPanel>
</DockPanel>
<TextBox
Grid.Row="1"
Margin="0,3,0,3"
VerticalAlignment="Center"
Text="TextBox"
TextWrapping="Wrap" />
<ListView
Grid.Row="2"
d:ItemsSource="{d:SampleData ItemCount=50}"
ItemsSource="{Binding Entities}"
SelectedItem="{Binding SelectedEntity}">
<ListView.View>
<GridView>
<GridViewColumn
Width="Auto"
DisplayMemberBinding="{Binding Id}"
Header="ID" />
<GridViewColumn DisplayMemberBinding="{Binding Name}" Header="Название" />
<GridViewColumn
Width="Auto"
DisplayMemberBinding="{Binding Rarity}"
Header="Редк." />
</GridView>
</ListView.View>
</ListView>
</Grid>
</UserControl>
@@ -0,0 +1,20 @@
namespace Pf2e.WPF.EntityViewer.Component;
/// <summary>
/// Логика взаимодействия для DbEntityListControl.xaml
/// </summary>
internal partial class EntityListControl : System.Windows.Controls.UserControl
{
public EntityListControl()
{
InitializeComponent();
}
private void TextBox_PreviewTextInput(
object sender,
System.Windows.Input.TextCompositionEventArgs e
)
{
e.Handled = !int.TryParse(e.Text, out int result);
}
}
@@ -0,0 +1,171 @@
<UserControl
x:Class="Pf2e.WPF.EntityViewer.Component.ViewControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:component="clr-namespace:Pf2e.WPF.EntityViewer.Component"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fa="http://schemas.awesome.incremented/wpf/xaml/fontawesome.sharp"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:struct="clr-namespace:Pf2e.WPF.EntityViewer.EntityViewStructure"
xmlns:viewmodel="clr-namespace:Pf2e.WPF.EntityViewer.ViewModel"
x:Name="_this"
d:DataContext="{d:DesignInstance Type=viewmodel:ViewViewModel}"
d:DesignHeight="450"
d:DesignWidth="300"
x:ClassModifier="internal"
mc:Ignorable="d">
<UserControl.Resources>
<FontFamily x:Key="FontAwesome">/FontAwesome.Sharp;component/fonts/#Font Awesome 6 Free Solid</FontFamily>
<struct:ColorAdapterConverter x:Key="ColorAdapterConverter" />
<Style x:Key="ButtonStyle" TargetType="Button">
<Setter Property="FontSize" Value="{Binding ButtonSize, ElementName=_this}" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="Width" Value="{Binding ButtonSize, ElementName=_this}" />
<Setter Property="Height" Value="{Binding ButtonSize, ElementName=_this}" />
<Setter Property="BorderThickness" Value="2,2,1,1" />
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="{DynamicResource {x:Static SystemColors.ControlColorKey}}" />
</Setter.Value>
</Setter>
<Setter Property="BorderBrush">
<Setter.Value>
<SolidColorBrush Color="{DynamicResource {x:Static SystemColors.ControlColorKey}}" />
</Setter.Value>
</Setter>
<Setter Property="Foreground" Value="Black" />
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="DarkSlateGray" />
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="Transparent" />
</Setter.Value>
</Setter>
<Setter Property="BorderBrush">
<Setter.Value>
<SolidColorBrush Color="Transparent" />
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
<Style
x:Key="FaButtonStyle"
BasedOn="{StaticResource ButtonStyle}"
TargetType="Button">
<Setter Property="FontFamily" Value="{StaticResource FontAwesome}" />
</Style>
<Style x:Key="HoverRow" TargetType="Grid">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlDarkBrush}}" />
</Trigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<UserControl.BorderBrush>
<SolidColorBrush Color="{DynamicResource {x:Static SystemColors.ActiveBorderColorKey}}" />
</UserControl.BorderBrush>
<UserControl.Background>
<SolidColorBrush Color="{DynamicResource {x:Static SystemColors.ControlColorKey}}" />
</UserControl.Background>
<ScrollViewer>
<ItemsControl AlternationCount="2" ItemsSource="{Binding Rows}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Grid.IsSharedSizeScope="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid
x:Name="FooBar"
Height="{Binding ItemHeight, ElementName=_this}"
Visibility="{Binding ItemVisibility}">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="ColumnOne" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal">
<component:ViewLadder
Width="65"
Height="{Binding ItemHeight, ElementName=_this}"
DepthSize="{Binding DepthSize, ElementName=_this}"
Ladder="{Binding Ladder}" />
<Button
Margin="-20,0,0,0"
Padding="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Command="{Binding CollapseCommand}"
Style="{StaticResource FaButtonStyle}"
Visibility="{Binding ButtonVisibility}">
<TextBlock Margin="-3,-3,0,0" Text="{Binding ButtonIcon}" />
</Button>
<TextBlock
Margin="2,0,10,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Name}" />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<TextBlock
Margin="0,0,5,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="{Binding TypeFontSize, ElementName=_this}"
FontStyle="Italic"
Text="{Binding Type}">
<TextBlock.Foreground>
<MultiBinding Converter="{StaticResource ColorAdapterConverter}">
<Binding Path="AccentColor" />
</MultiBinding>
</TextBlock.Foreground>
</TextBlock>
<TextBox
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="Transparent"
BorderThickness="0"
IsReadOnly="True"
Text="{Binding Value, Mode=OneWay}">
<TextBox.Foreground>
<MultiBinding Converter="{StaticResource ColorAdapterConverter}">
<Binding Path="AccentColor" />
</MultiBinding>
</TextBox.Foreground>
</TextBox>
</StackPanel>
</Grid>
<DataTemplate.Triggers>
<Trigger Property="ItemsControl.AlternationIndex" Value="0">
<Setter TargetName="FooBar" Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlDarkBrush}}" />
</Trigger>
<Trigger Property="ItemsControl.AlternationIndex" Value="1">
<Setter TargetName="FooBar" Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlBrush}}" />
</Trigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</UserControl>
@@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Pf2e.Scrapper.Helpers;
namespace Pf2e.WPF.EntityViewer.Component;
internal partial class ViewControl : UserControl
{
#region "Поля элемента управления"
/// <summary>
/// Высота строки поля структуры.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[Category("Entity Render")]
[Description("The height of each item in the tree view.")]
public double ItemHeight
{
get { return (double)GetValue(ItemHeightProperty); }
set { SetValue(ItemHeightProperty, value); }
}
public static readonly DependencyProperty ItemHeightProperty = DependencyProperty.Register(
"ItemHeight",
typeof(double),
typeof(ViewControl),
new PropertyMetadata(20.0)
);
/// <summary>
/// Ширина отступа глубины.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[Category("Entity Render")]
[Description("The size of tab.")]
public double DepthSize
{
get { return (double)GetValue(DepthSizeProperty); }
set { SetValue(DepthSizeProperty, value); }
}
public static readonly DependencyProperty DepthSizeProperty = DependencyProperty.Register(
"DepthSize",
typeof(double),
typeof(ViewControl),
new PropertyMetadata(20.0)
);
/// <summary>
/// Размер шрифта типа поля сущности.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[Category("Entity Render")]
[Description("The size of tab.")]
public double TypeFontSize
{
get { return (double)GetValue(TypeFontSizeProperty); }
set { SetValue(TypeFontSizeProperty, value); }
}
public static readonly DependencyProperty TypeFontSizeProperty = DependencyProperty.Register(
"TypeFontSize",
typeof(double),
typeof(ViewControl),
new PropertyMetadata(9.5)
);
/// <summary>
/// Размер кнопки плюс/минус.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[Category("Entity Render")]
[Description("The size of plus/minus button.")]
public double ButtonSize
{
get { return (double)GetValue(ButtonSizeProperty); }
set { SetValue(ButtonSizeProperty, value); }
}
public static readonly DependencyProperty ButtonSizeProperty = DependencyProperty.Register(
"ButtonSize",
typeof(double),
typeof(ViewControl),
new PropertyMetadata(12.0)
);
#endregion
public ViewControl()
{
InitializeComponent();
}
}
@@ -0,0 +1,13 @@
<UserControl
x:Class="Pf2e.WPF.EntityViewer.Component.ViewLadder"
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:local="clr-namespace:Pf2e.WPF.EntityViewer.Component"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="20"
d:DesignWidth="60"
x:ClassModifier="internal"
mc:Ignorable="d">
<Canvas x:Name="TargetCanvas" />
</UserControl>
@@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Pf2e.WPF.EntityViewer;
using Pf2e.WPF.EntityViewer.EntityViewStructure;
namespace Pf2e.WPF.EntityViewer.Component;
/// <summary>
/// Логика взаимодействия для ViewLadder.xaml
/// </summary>
internal partial class ViewLadder : UserControl
{
/// <summary>
/// Ширина отступа глубины.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[Category("Entity Render")]
[Description("The size of tab.")]
public double DepthSize
{
get { return (double)GetValue(DepthSizeProperty); }
set { SetValue(DepthSizeProperty, value); }
}
public static readonly DependencyProperty DepthSizeProperty = DependencyProperty.Register(
"DepthSize",
typeof(double),
typeof(ViewLadder),
new FrameworkPropertyMetadata(20.0, new PropertyChangedCallback(OnValueChanged))
);
public IEnumerable<LadderType> Ladder
{
get { return (IEnumerable<LadderType>)GetValue(LadderProperty); }
set { SetValue(LadderProperty, value); }
}
public static readonly DependencyProperty LadderProperty = DependencyProperty.Register(
"Ladder",
typeof(IEnumerable<LadderType>),
typeof(ViewLadder),
new FrameworkPropertyMetadata(
Array.Empty<LadderType>(),
new PropertyChangedCallback(OnValueChanged)
)
);
private static void OnValueChanged(
DependencyObject obj,
DependencyPropertyChangedEventArgs args
)
{
ViewLadder control = (ViewLadder)obj;
control.Rebuild();
}
public ViewLadder()
{
InitializeComponent();
}
public void Rebuild()
{
int n = Ladder.Count();
Width = n * DepthSize;
DrawingGroup result = new();
result.Children.Add(
new GeometryDrawing()
{
Geometry = new RectangleGeometry(new(0, 0, n * DepthSize, Height)),
Pen = new(Brushes.Transparent, 0),
}
);
result.Children.Add(
new GeometryDrawing()
{
Geometry = GetLadderGeometryGroup(Ladder, DepthSize, Height),
Pen = new(Brushes.Black, 1),
}
);
TargetCanvas.Background = new DrawingBrush(result);
}
private GeometryGroup GetLadderGeometryGroup(IEnumerable<LadderType> ladder, double w, double h)
{
GeometryGroup geometryGroup = new();
double w2 = w / 2;
double h2 = h / 2;
int i = 0;
foreach (LadderType ladderType in ladder)
{
switch (ladderType)
{
case LadderType.Empty:
break;
case LadderType.Last:
geometryGroup.Children.Add(
new LineGeometry(new(i * w + w2, 0), new(i * w + w2, h2))
);
geometryGroup.Children.Add(
new LineGeometry(new(i * w + w2, h2), new(i * w + w, h2))
);
break;
case LadderType.Through:
geometryGroup.Children.Add(
new LineGeometry(new(i * w + w2, 0), new(i * w + w2, h))
);
if (i == ladder.Count() - 1)
{
geometryGroup.Children.Add(
new LineGeometry(new(i * w + w2, h2), new(i * w + w, h2))
);
}
break;
default:
throw new ArgumentOutOfRangeException(nameof(ladder));
}
i++;
}
return geometryGroup;
}
}
+88
View File
@@ -0,0 +1,88 @@
<Window
x:Class="Pf2e.WPF.EntityViewer.EntityForm"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:component="clr-namespace:Pf2e.WPF.EntityViewer.Component"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewmodel="clr-namespace:Pf2e.WPF.EntityViewer.ViewModel"
Title="EntityForm"
Width="1400"
Height="800"
d:DataContext="{d:DesignInstance Type=viewmodel:EntityFormViewModel}"
x:ClassModifier="internal"
Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"
Loaded="Window_Loaded"
mc:Ignorable="d">
<Grid ClipToBounds="True">
<Grid.RowDefinitions>
<RowDefinition Height="25" />
<RowDefinition Height="150" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="215*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="537*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="322*" />
</Grid.ColumnDefinitions>
<GridSplitter
Grid.Row="0"
Grid.RowSpan="4"
Grid.Column="1"
Width="5"
HorizontalAlignment="Stretch" />
<GridSplitter
Grid.Row="0"
Grid.RowSpan="4"
Grid.Column="3"
Width="5"
HorizontalAlignment="Stretch" />
<GridSplitter
Grid.Row="2"
Grid.Column="0"
Height="5"
HorizontalAlignment="Stretch"
Cursor="SizeNS"
ResizeDirection="Rows" />
<Button
Margin="3,4,3,0"
Click="Button_Click"
Content="&lt;&lt;&lt; Вернуться в начало" />
<ListBox
x:Name="EntityTypeListBox"
Grid.Row="1"
Grid.Column="0"
Margin="3,3,3,0"
d:ItemsSource="{d:SampleData ItemCount=10}"
ItemsSource="{Binding EntityTypes}"
SelectedItem="{Binding SelectedEntityType}" />
<component:EntityListControl
x:Name="EntityListControl"
Grid.Row="2"
Grid.RowSpan="2"
Grid.Column="0"
ClipToBounds="True" />
<component:ViewControl
x:Name="ViewControl"
Grid.Row="0"
Grid.RowSpan="4"
Grid.Column="2"
FontFamily="Consolas" />
<TextBox
Grid.Row="0"
Grid.RowSpan="4"
Grid.Column="4"
IsReadOnly="True"
Text="{Binding SelectedEntityDescription, Mode=OneWay}"
TextWrapping="Wrap" />
</Grid>
</Window>
+36
View File
@@ -0,0 +1,36 @@
using System.Collections.ObjectModel;
using System.Windows;
using Pf2e.Database;
using Pf2e.Database.Model.Enum;
using Pf2e.Scrapper.WPF.EntityViewer.ViewModel;
namespace Pf2e.WPF.EntityViewer;
internal partial class EntityForm : Window
{
public Pf2eDbContext Context { get; }
public EntityFormViewModel ViewModel { get; }
public EntityForm(Pf2eDbContext context, EntityFormViewModel viewModel)
{
Context = context;
ViewModel = viewModel;
InitializeComponent();
EntityListControl.DataContext = ViewModel.DbEntityListViewModel;
ViewControl.DataContext = ViewModel.ViewViewModel;
DataContext = ViewModel;
ViewModel.DbEntityListViewModel.Reload();
}
private void Window_Loaded(object sender, RoutedEventArgs e) { }
private void Button_Click(object sender, RoutedEventArgs e)
{
ApplicationStateChange?.Invoke(this, ApplicationStage.InitialSettings);
}
public event ImportApplication.ApplicationStateChangeDelegate? ApplicationStateChange;
}
@@ -0,0 +1,22 @@
namespace Pf2e.WPF.EntityViewer.EntityViewStructure;
/// <summary>
/// Тип лестницы глубины.
/// </summary>
internal enum LadderType
{
/// <summary>
/// Пустой элемент: [ ].
/// </summary>
Empty,
/// <summary>
/// Последний элемент: [└].
/// </summary>
Last,
/// <summary>
/// Проходной элемент: [├] или [│].
/// </summary>
Through,
}
@@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Pf2e.WPF.EntityViewer.EntityViewStructure;
/// <summary>
/// Класс для построения структуры данных класса.
/// </summary>
internal class StructureBuilder
{
/// <summary>
/// Список обработанных объектов.
/// </summary>
private readonly List<StructureProcessedObject> _processedObjects = [];
/// <summary>
/// Создание поля структуры.
/// </summary>
/// <param name="name">Название поля.</param>
/// <param name="value">Значение поля.</param>
/// <param name="depth">Глубина, на котором поле отображается.</param>
/// <returns>Поле структуры.</returns>
public StructureFieldComplex Create(string name, object value, int depth = 0)
{
_ = RegisterProcessed(value);
StructureFieldComplex field = new(this, value, name, depth);
CompleteProcessed(value, field);
return field;
}
/// <summary>
/// Регистрация обработанного объекта.
/// </summary>
/// <param name="value">Регистрируемый объект.</param>
/// <returns>`true`, если объект был добавлен; `false`, если объект был добавлен ранее.</returns>
public bool RegisterProcessed(object value)
{
if (_processedObjects.Any(p => p.Value == value))
{
return false;
}
StructureProcessedObject processed = new(value);
_processedObjects.Add(processed);
return true;
}
/// <summary>
/// Дополнить обработанный объект.
/// </summary>
/// <param name="value">Значение.</param>
/// <param name="field">Поле объекта.</param>
public void CompleteProcessed(object value, StructureFieldComplex field)
{
StructureProcessedObject? q = _processedObjects.FirstOrDefault(p => p.Value == value);
if (q == null)
{
return;
}
q.Field = field;
}
/// <summary>
/// Выбор поля структуры по значению.
/// </summary>
/// <param name="name">Название поля.</param>
/// <param name="value">Значение поля.</param>
/// <param name="depth">Глубина, на котором поле отображается.</param>
/// <returns></returns>
public StructureField Resolve(string name, object? value, int depth)
{
switch (GetFieldType(value))
{
case StructureFieldType.Null:
return new StructureFieldNull(this, name, depth);
case StructureFieldType.Primitive:
return new StructureFieldPrimitive(this, value!, name, depth);
case StructureFieldType.Enum:
return new StructureFieldEnum(this, value!, name, depth);
case StructureFieldType.Reference:
break;
case StructureFieldType.Complex:
default:
StructureProcessedObject? processed = _processedObjects.FirstOrDefault(p =>
p.Value == value
);
if (processed is null)
{
return Create(name, value!, depth);
}
StructureFieldReference reference = new(this, name, null, depth);
processed.AddReference(reference);
return reference;
}
throw new("Impossible");
}
/// <summary>
/// Получить тип поля структуры по значению.
/// </summary>
/// <param name="value">Значение поля.</param>
/// <returns>Тип поля структуры.</returns>
public static StructureFieldType GetFieldType(object? value)
{
if (value is null)
{
return StructureFieldType.Null;
}
Type type = value.GetType();
if (type.IsEnum)
{
return StructureFieldType.Enum;
}
if (type.IsPrimitive || type == typeof(string))
{
return StructureFieldType.Primitive;
}
return StructureFieldType.Complex;
}
}
@@ -0,0 +1,248 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
namespace Pf2e.WPF.EntityViewer.EntityViewStructure;
/// <summary>
/// Базовый класс для полей структуры.
/// </summary>
/// <param name="builder">Класс построения структуры данных.</param>
/// <param name="name">Название поля.</param>
/// <param name="depth">Глубина поля.</param>
internal abstract class StructureField(StructureBuilder builder, string name, int depth)
: INotifyPropertyChanged
{
/// <summary>
/// Название поля.
/// </summary>
public string Name { get; } = name;
/// <summary>
/// Тип поля структуры.
/// </summary>
public abstract StructureFieldType FieldType { get; }
/// <summary>
/// Класс построения структуры данных.
/// </summary>
public StructureBuilder Builder { get; } = builder;
/// <summary>
/// Флаг, указывающий, отображается ли поле.
/// </summary>
public virtual bool Visible
{
get => _visible;
set
{
_visible = value;
OnPropertyChanged();
}
}
private bool _visible = true;
/// <summary>
/// Глубина поля структуры.
/// </summary>
public int Depth { get; } = depth;
/// <summary>
/// Получить строку, представляющую тип поля.
/// </summary>
/// <returns>Строка, представляющая тип поля.</returns>
public abstract string GetTypeString();
/// <summary>
/// Получить строку, представляющую значение поля.
/// </summary>
/// <returns>Строка, представляющая значение поля.</returns>
public abstract string GetValueString();
/// <summary>
/// Получить набор строк, используя результат функция.
/// </summary>
/// <typeparam name="T">Тип результирующей строки.</typeparam>
/// <param name="func">Функция, оборажения поля в результирующую строку.</param>
/// <returns>Коллекция, содержащая набор строк.</returns>
public virtual ICollection<T> GetRows<T>(Func<StructureField, T> func)
{
StructureField[] q = [this];
return q.Select(func).ToList();
}
/// <summary>
/// Событие изменения поля.
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;
/// <summary>
/// Запустить событие изменения поля.
/// </summary>
/// <param name="propertyName">Название поля.</param>
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new(propertyName));
}
}
/// <summary>
/// Поле структуры с перечислением.
/// </summary>
/// <param name="builder">Класс построения структуры данных.</param>
/// <param name="value">Значение поля.</param>
/// <param name="name">Название поля.</param>
/// <param name="depth">Глубина поля.</param>
internal class StructureFieldEnum(StructureBuilder builder, object value, string name, int depth)
: StructureField(builder, name, depth)
{
/// <inheritdoc/>
public override StructureFieldType FieldType => StructureFieldType.Enum;
/// <summary>
/// Значение поля.
/// </summary>
public readonly object Value = value;
/// <summary>
/// Тип значения поля.
/// </summary>
public readonly Type Type = value.GetType();
/// <inheritdoc/>
public override string GetTypeString() => "enum";
/// <inheritdoc/>
public override string GetValueString() => $"{Type.Name}.{Value}";
}
/// <summary>
/// Пустое поле структуры.
/// </summary>
/// <param name="builder">Класс построения структуры данных.</param>
/// <param name="name">Название поля.</param>
/// <param name="depth">Глубина поля.</param>
internal class StructureFieldNull(StructureBuilder builder, string name, int depth)
: StructureField(builder, name, depth)
{
/// <inheritdoc/>
public override StructureFieldType FieldType => StructureFieldType.Null;
/// <inheritdoc/>
public override string GetTypeString() => "null";
/// <inheritdoc/>
public override string GetValueString() => "";
}
/// <summary>
/// Простое поле структуры.
/// </summary>
/// <param name="builder">Класс построения структуры данных.</param>
/// <param name="value">Значение поля.</param>
/// <param name="name">Название поля.</param>
/// <param name="depth">Глубина поля.</param>
internal class StructureFieldPrimitive(
StructureBuilder builder,
object value,
string name,
int depth
) : StructureField(builder, name, depth)
{
/// <inheritdoc/>
public override StructureFieldType FieldType => StructureFieldType.Primitive;
/// <summary>
/// Значение поля.
/// </summary>
public readonly object Value = value;
/// <summary>
/// Тип значения поля.
/// </summary>
public readonly Type Type = value.GetType();
/// <inheritdoc/>
public override string GetTypeString()
{
return Type.GetTypeCode(Type) switch
{
TypeCode.Boolean => "bool",
TypeCode.Int32 => "int",
TypeCode.String => "string",
TypeCode.Double => "double",
_ => Type.Name,
};
}
/// <inheritdoc/>
public override string GetValueString()
{
if (Type == typeof(string))
{
return "\"" + ((string)Value).Replace("\"", "\\\"") + "\"";
}
return Value.ToString() ?? string.Empty;
}
}
/// <summary>
/// Поле структуры с ссылкой на объект.
/// </summary>
/// <param name="builder">Класс построения структуры данных.</param>
/// <param name="name">Название поля.</param>
/// <param name="reference">Ссылка на объект.</param>
/// <param name="depth">Глубина поля.</param>
internal class StructureFieldReference(
StructureBuilder builder,
string name,
StructureFieldComplex? reference,
int depth
) : StructureField(builder, name, depth)
{
/// <summary>
/// Ссылка на объект.
/// </summary>
public StructureFieldComplex? Reference = reference;
/// <inheritdoc/>
public override StructureFieldType FieldType => StructureFieldType.Reference;
/// <inheritdoc/>
public override string GetTypeString() => Reference?.GetTypeString() ?? "???";
/// <inheritdoc/>
public override string GetValueString() => "<Reference>";
}
/// <summary>
/// Поле структуры пропускаемого объекта.
/// </summary>
/// <param name="builder">Класс построения структуры данных.</param>
/// <param name="name">Название поля.</param>
/// <param name="reference">Ссылка на объект.</param>
/// <param name="depth">Глубина поля.</param>
internal class StructureFieldIgnored(
StructureBuilder builder,
string name,
StructureFieldComplex? reference,
int depth
) : StructureField(builder, name, depth)
{
/// <summary>
/// Ссылка на объект.
/// </summary>
public StructureFieldComplex? Reference = reference;
/// <inheritdoc/>
public override StructureFieldType FieldType => StructureFieldType.Reference;
/// <inheritdoc/>
public override string GetTypeString() => Reference?.GetTypeString() ?? "???";
/// <inheritdoc/>
public override string GetValueString() => "<Ignored>";
}
@@ -0,0 +1,239 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using LinqToDB.Extensions;
using Pf2e.Database.Model.Common.I18n;
namespace Pf2e.WPF.EntityViewer.EntityViewStructure;
/// <summary>
/// Поле стркутуры с объектом.
/// </summary>
internal class StructureFieldComplex : StructureField
{
/// <inheritdoc/>
public override StructureFieldType FieldType => StructureFieldType.Complex;
/// <summary>
/// Набор полей объекта.
/// </summary>
public readonly List<StructureField> Fields = [];
/// <summary>
/// Тип значения поля.
/// </summary>
public readonly Type Type;
/// <summary>
/// Флаг, указывающий, отображается ли поля объекта.
/// </summary>
public bool Collapsed
{
get => _collapsed;
set
{
_collapsed = value;
UpdateFieldVisibility();
OnPropertyChanged();
}
}
private bool _collapsed;
/// <summary>
/// Флаг, указывающий, что у этот элемент можно свернуть.
/// </summary>
public bool Collapsable
{
get => Fields.Count > 0;
}
/// <summary>
/// Значение поля идентификатора элемента.
/// </summary>
public string? IdValue { get; private set; }
/// <inheritdoc/>
public override bool Visible
{
get => base.Visible;
set
{
base.Visible = value;
UpdateFieldVisibility();
}
}
/// <summary>
/// Конструктор класса поля структуры с объектом.
/// </summary>
/// <param name="builder">Класс построения структуры данных.</param>
/// <param name="value">Значение поля.</param>
/// <param name="name">Название поля.</param>
/// <param name="depth">Глубина поля.</param>
public StructureFieldComplex(StructureBuilder builder, object value, string name, int depth)
: base(builder, name, depth)
{
Type = value.GetType();
SetFields((dynamic)value);
}
/// <summary>
/// Обновить видимость полей структуры.
/// </summary>
private void UpdateFieldVisibility()
{
foreach (StructureField field in Fields)
{
field.Visible = Visible && !Collapsed;
}
}
/// <summary>
/// Установить поля структуры `LocaleString`.
/// </summary>
/// <param name="localeString">Объект строки локализации.</param>
private void SetFields(LocaleString localeString)
{
SetFieldsFromRecords(
localeString
.Select(field => new FieldRecord(field.Locale.ToString(), field.Value))
.ToList()
);
}
/// <summary>
/// Установить поля структуры `IEnumerable`.
/// </summary>
/// <param name="enumerable">Объект-перечисление.</param>
private void SetFields(IEnumerable enumerable)
{
int i = 0;
List<FieldRecord> records = [];
foreach (object? item in enumerable)
{
records.Add(new($"[{i++}]", item));
}
SetFieldsFromRecords(records);
}
/// <summary>
/// Установить поля структуры общего объекта.
/// </summary>
/// <param name="value">Объект.</param>
private void SetFields(object value)
{
Type type = value.GetType();
PropertyInfo[] properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
var records = properties
.Where(property => property.GetAttribute<Newtonsoft.Json.JsonIgnoreAttribute>() is null)
.Select(property => new FieldRecord(property.Name, property.GetValue(value, null)))
.ToList();
var idRecord = records.FirstOrDefault(record => record.Name == "Id");
if (idRecord is not null && idRecord.Value is not null)
{
records.Remove(idRecord);
IdValue = idRecord.Value.ToString();
}
SetFieldsFromRecords(records);
}
/// <summary>
/// Установить поля структуры из списка записей.
/// </summary>
/// <param name="records"></param>
private void SetFieldsFromRecords(List<FieldRecord> records)
{
foreach (
FieldRecord record in records.Where(record =>
record.FieldType == StructureFieldType.Complex
)
)
{
record.Allowed = Builder.RegisterProcessed(record.Value!);
}
foreach (FieldRecord record in records)
{
Fields.Add(
record is { FieldType: StructureFieldType.Complex, Allowed: true }
? Builder.Create(record.Name, record.Value!, Depth + 1)
: Builder.Resolve(record.Name, record.Value!, Depth + 1)
);
}
}
/// <summary>
/// Запись поля структуры.
/// </summary>
/// <param name="name">Название поля.</param>
/// <param name="value">Значение поля.</param>
private class FieldRecord(string name, object? value)
{
/// <summary>
/// Название поля.
/// </summary>
public readonly string Name = name;
/// <summary>
/// Значение поля.
/// </summary>
public readonly object? Value = value;
/// <summary>
/// Тип поля структуры.
/// </summary>
public StructureFieldType FieldType => StructureBuilder.GetFieldType(Value);
/// <summary>
/// Флаг, указывающий, использовать ли поле.
/// </summary>
public bool Allowed = true;
}
/// <inheritdoc/>
public override string GetTypeString()
{
if (Type.IsGenericEnumerableType())
{
Type elementType = Type.GetGenericArguments().Single();
return $"{elementType.Name}[]";
}
else
{
return Type.Name;
}
}
/// <inheritdoc/>
public override string GetValueString()
{
if (Type.IsGenericEnumerableType())
{
return Fields.Count.ToString() + (Fields.Count == 1 ? " item" : " items");
}
if (IdValue is not null)
{
return $"#{IdValue}";
}
return "";
}
/// <inheritdoc/>
public override ICollection<T> GetRows<T>(Func<StructureField, T> func)
{
return [.. base.GetRows(func), .. Fields.SelectMany(f => f.GetRows(func)).ToList()];
}
}
@@ -0,0 +1,32 @@
namespace Pf2e.WPF.EntityViewer.EntityViewStructure;
/// <summary>
/// Тип поля структуры.
/// </summary>
internal enum StructureFieldType
{
/// <summary>
/// Пустое поле.
/// </summary>
Null,
/// <summary>
/// Простое поле.
/// </summary>
Primitive,
/// <summary>
/// Поле с перечислением.
/// </summary>
Enum,
/// <summary>
/// Поле с объектом.
/// </summary>
Complex,
/// <summary>
/// Поле с ссылкой на объект.
/// </summary>
Reference,
}
@@ -0,0 +1,39 @@
using System.Collections.Generic;
namespace Pf2e.WPF.EntityViewer.EntityViewStructure;
/// <summary>
/// Класс для хранения обработанного объекта.
/// </summary>
/// <param name="value">Объект.</param>
internal class StructureProcessedObject(object value)
{
public readonly object Value = value;
private StructureFieldComplex? _field;
public StructureFieldComplex? Field
{
get => _field;
set
{
_field = value;
foreach (StructureFieldReference reference in _references)
{
reference.Reference = Field;
}
}
}
private readonly List<StructureFieldReference> _references = [];
public void AddReference(StructureFieldReference reference)
{
_references.Add(reference);
if (_field is not null)
{
reference.Reference = Field;
}
}
}
@@ -0,0 +1,271 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using FontAwesome.Sharp;
using Color = System.Windows.Media.Color;
namespace Pf2e.WPF.EntityViewer.EntityViewStructure;
/// <summary>
/// Строка структуры, представляющая поле сущности.
/// </summary>
internal class StructureRow : INotifyPropertyChanged
{
/// <summary>
/// Отображаемая строка поля сущности.
/// </summary>
public StructureField Field { get; }
/// <summary>
/// Список типов лестницы.
/// </summary>
public List<LadderType> Ladder { get; set; } = [];
/// <summary>
/// Флаг наличия кнопки.
/// </summary>
public bool HasButton =>
Field.FieldType == StructureFieldType.Complex && ((StructureFieldComplex)Field).Collapsable;
/// <summary>
/// Флаг, указывающий, отображается ли строка.
/// </summary>
public bool Visible => Field.Visible;
/// <summary>
/// Флаг, указывающий, отображается ли строка.
/// </summary>
public Visibility ItemVisibility => Visible ? Visibility.Visible : Visibility.Collapsed;
/// <summary>
/// Флаг, указывающий, отображается ли строка.
/// </summary>
public Visibility ButtonVisibility => HasButton ? Visibility.Visible : Visibility.Collapsed;
/// <summary>
/// Иконка кнопки.
/// </summary>
public string ButtonIcon =>
HasButton && !((StructureFieldComplex)Field).Collapsed
? IconChar.MinusSquare.ToChar()
: IconChar.PlusSquare.ToChar();
/// <summary>
/// Глубина отображения поля.
/// </summary>
public int Depth => Field.Depth;
/// <summary>
/// Строковое представление названия поля.
/// </summary>
public string Name => Field.Name;
/// <summary>
/// Строковое представление типа поля.
/// </summary>
public string Type
{
get
{
string result = Field.GetTypeString();
if (result != "")
{
result = $@"({result})";
}
return result;
}
}
/// <summary>
/// Строковое представление значения поля.
/// </summary>
public string Value => Field.GetValueString().Replace("\r", "").Replace("\n", "");
/// <summary>
/// Цвет значения
/// </summary>
public Color? AccentColor =>
Field switch
{
StructureFieldPrimitive primitive => System.Type.GetTypeCode(primitive.Type) switch
{
TypeCode.Boolean => Colors.DarkGoldenrod,
TypeCode.Int32 => Colors.Blue,
TypeCode.Double => Colors.Aqua,
TypeCode.String => Colors.Green,
_ => null,
},
StructureFieldEnum => Colors.Red,
StructureFieldNull => Colors.Transparent,
_ => null,
};
/// <summary>
/// Команда переключения.
/// </summary>
public ICommand CollapseCommand { get; }
/// <summary>
/// Кониструктор класса <see cref="StructureRow"/>.
/// </summary>
/// <param name="field">Поле.</param>
public StructureRow(StructureField field)
{
Field = field;
CollapseCommand = new CollapseCommand(this);
Field.PropertyChanged += Field_PropertyChanged;
}
private void Field_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
OnPropertyChanged(nameof(Field));
OnPropertyChanged(nameof(Visible));
OnPropertyChanged(nameof(ItemVisibility));
OnPropertyChanged(nameof(ButtonVisibility));
OnPropertyChanged(nameof(ButtonIcon));
}
/// <summary>
/// Событие изменения поля.
/// </summary>
public event Action<StructureRow, StructureField>? OnChange;
/// <summary>
/// Изменение поля.
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;
/// <summary>
/// При изменении поля.
/// </summary>
/// <param name="propertyName">Название поля.</param>
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new(propertyName));
}
}
/// <summary>
/// Команда для сворачивания/разворачивания поля структуры.
/// </summary>
/// <param name="structureRow">Строка структуры, содержащая поле.</param>
internal class CollapseCommand(StructureRow structureRow) : ICommand
{
/// <summary>
/// Строка структуры, содержащая поле, для которого выполняется команда.
/// </summary>
private StructureRow StructureRow { get; } = structureRow;
/// <summary>
/// Событие, вызываемое при изменении возможности выполнения команды.
/// </summary>
public event EventHandler? CanExecuteChanged;
/// <summary>
/// Проверяет, может ли команда быть выполнена с указанным параметром.
/// </summary>
/// <param name="parameter">Параметр, передаваемый команде.</param>
/// <returns><c>true</c>, если команда может быть выполнена; в противном случае <c>false</c>.</returns>
public bool CanExecute(object? parameter)
{
return true;
}
/// <summary>
/// Выполняет команду с указанным параметром.
/// </summary>
/// <param name="parameter">Параметр, передаваемый команде.</param>
public void Execute(object? parameter)
{
StructureFieldComplex field = (StructureFieldComplex)StructureRow.Field;
field.Collapsed = !field.Collapsed;
}
}
/// <summary>
/// Преобразователь, который адаптирует цвет в зависимости от типа целевого свойства.
/// </summary>
public class ColorAdapterConverter : IMultiValueConverter
{
/// <summary>
/// Смешивает два компнента цвета с заданным коэффициентом смешивания.
/// </summary>
/// <param name="a">Первый компонент цвета.</param>
/// <param name="b">Второй компонент цвета.</param>
/// <param name="rate">Коэффициент смешивания, где 0.0 означает полный компонент <paramref name="a"/>, а 1.0 означает полный компонент <paramref name="b"/>.</param>
/// <returns>Смешанный компонент цвета.</returns>
private byte Mix(byte a, byte b, double rate) =>
System.Convert.ToByte(Math.Round(a * rate + b * (1 - rate)));
/// <summary>
/// Преобразует массив значений в целевой тип, усредняя цвета.
/// </summary>
/// <param name="value">Массив значений, содержащий цвета.</param>
/// <param name="targetType">Целевой тип, в который нужно преобразовать значения.</param>
/// <param name="parameter">Параметр преобразования, не используется.</param>
/// <param name="culture">Культура, используемая для преобразования, не используется.</param>
/// <returns>Результат преобразования в целевой тип, который может быть <c>null</c>, если преобразование невозможно.</returns>
public object? Convert(object[] value, Type targetType, object parameter, CultureInfo culture)
{
Color textColor = SystemColors.WindowTextColor;
double a = textColor.A;
double r = textColor.R;
double g = textColor.G;
double b = textColor.B;
int n = 1 + value.Length;
foreach (object item in value)
{
if (item is not Color color)
{
n--;
continue;
}
a += color.A;
r += color.R;
g += color.G;
b += color.B;
}
Color resultColor = Color.FromArgb(
System.Convert.ToByte(Math.Round(a / n)),
System.Convert.ToByte(Math.Round(r / n)),
System.Convert.ToByte(Math.Round(g / n)),
System.Convert.ToByte(Math.Round(b / n))
);
Dictionary<Type, Func<object>> @switch = new()
{
{ typeof(Brush), () => new SolidColorBrush(resultColor) },
};
return @switch.TryGetValue(targetType, out Func<object>? action) ? action() : null;
}
/// <summary>
/// Преобразует значение обратно в массив типов, но не реализован.
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public object[] ConvertBack(
object value,
Type[] targetType,
object parameter,
CultureInfo culture
)
{
throw new NotImplementedException();
}
}
@@ -0,0 +1,31 @@
using System;
using Pf2e.Database.Model.Base;
namespace Pf2e.WPF.EntityViewer.Stores;
/// <summary>
/// Хранилище выбранной сущности.
/// </summary>
public class SelectedEntityStore
{
/// <exclude />
private Entity? _selectedEntity;
/// <summary>
/// Сущность, выбранная в интерфейсе.
/// </summary>
public Entity? SelectedEntity
{
get => _selectedEntity;
set
{
_selectedEntity = value;
SelectedEntityChanged?.Invoke(SelectedEntity);
}
}
/// <summary>
/// Событие, вызываемое при изменении выбранной сущности.
/// </summary>
public event Action<Entity?>? SelectedEntityChanged;
}
@@ -0,0 +1,21 @@
using System;
using Pf2e.Database.Model.Enum;
namespace Pf2e.WPF.EntityViewer.Stores;
internal class SelectedEntityTypeStore
{
private EntityType? _selectedEntityType;
public EntityType? SelectedEntityType
{
get => _selectedEntityType;
set
{
_selectedEntityType = value;
SelectedEntityTypeChanged?.Invoke(SelectedEntityType);
}
}
public event Action<EntityType?>? SelectedEntityTypeChanged;
}
@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Pf2e.Database;
using Pf2e.Database.Model.Base;
using Pf2e.Database.Model.Enum;
using Pf2e.Scrapper.WPF.EntityViewer.Stores;
namespace Pf2e.WPF.EntityViewer.ViewModel;
internal class EntityFormViewModel : ViewModelBase
{
#region "Конструктор"
public IServiceProvider ServiceProvider { get; }
public Pf2eDbContext Context { get; }
public EntityListViewModel DbEntityListViewModel { get; }
public ViewViewModel ViewViewModel { get; }
public SelectedEntityTypeStore SelectedEntityTypeStore { get; }
public SelectedEntityStore SelectedEntityStore { get; }
public EntityFormViewModel(
IServiceProvider serviceProvider,
Pf2eDbContext context,
EntityListViewModel entityListViewModel,
ViewViewModel viewViewModel,
SelectedEntityTypeStore selectedEntityTypeStore,
SelectedEntityStore selectedEntityStore
)
{
ServiceProvider = serviceProvider;
Context = context;
DbEntityListViewModel = entityListViewModel;
ViewViewModel = viewViewModel;
SelectedEntityTypeStore = selectedEntityTypeStore;
SelectedEntityStore = selectedEntityStore;
Update();
SelectedEntityStore.SelectedEntityChanged += SelectedEntityStore_SelectedEntityChanged;
}
public void Update()
{
LoadEntityTypes();
}
#endregion
#region "Выбор типа"
public IEnumerable<EntityType> EntityTypes { get; set; } = [];
public EntityType? SelectedEntityType
{
get => SelectedEntityTypeStore.SelectedEntityType;
set
{
SelectedEntityTypeStore.SelectedEntityType = value;
OnPropertyChanged();
}
}
private void LoadEntityTypes()
{
EntityTypes = Context.Entities.Select(e => e.Type).Distinct().Order().ToList();
OnPropertyChanged(nameof(EntityTypes));
SelectedEntityTypeStore.SelectedEntityType = EntityTypes.Any() ? EntityTypes.First() : null;
}
#endregion
#region "Текущий элемент"
public string SelectedEntityDescription
{
get => SelectedEntityStore.SelectedEntity?.Description ?? "";
}
private void SelectedEntityStore_SelectedEntityChanged(Entity? obj)
{
OnPropertyChanged(nameof(SelectedEntityDescription));
}
#endregion
public string TestText => SelectedEntityType?.ToString() ?? "null";
}
@@ -0,0 +1,242 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Pf2e.Database;
using Pf2e.Database.Model.Base;
using Pf2e.Database.Model.Enum;
using Pf2e.Scrapper.WPF.EntityViewer.Stores;
namespace Pf2e.WPF.EntityViewer.ViewModel;
internal class EntityListViewModel : ViewModelBase
{
public Pf2eDbContext Context { get; }
public SelectedEntityTypeStore SelectedEntityTypeStore { get; }
public SelectedEntityStore SelectedEntityStore { get; }
private EntityType? SelectedEntityType => SelectedEntityTypeStore.SelectedEntityType;
public Entity? SelectedEntity
{
get => SelectedEntityStore.SelectedEntity;
set => SelectedEntityStore.SelectedEntity = value;
}
public int Page
{
get => _page;
set
{
_page = Math.Clamp(value, 1, PageCount);
OnPropertyChanged();
OnPropertyChanged(nameof(HasPrevious));
OnPropertyChanged(nameof(HasNext));
Reload();
}
}
private int _page = 1;
public int EntitiesCount
{
get => _entitiesCount;
set
{
_entitiesCount = value;
OnPropertyChanged();
OnPropertyChanged(nameof(PageCount));
OnPropertyChanged(nameof(HasPrevious));
OnPropertyChanged(nameof(HasNext));
}
}
private int _entitiesCount = 1;
public int PerPage
{
get => _perPage;
set
{
_perPage = value;
OnPropertyChanged();
OnPropertyChanged(nameof(PageCount));
Reload();
}
}
private int _perPage = 30;
public string Search
{
get => _search;
set
{
_search = value;
OnPropertyChanged();
Reload();
}
}
private string _search = "";
public int PageCount => Convert.ToInt32(Math.Ceiling(1.0 * EntitiesCount / PerPage));
public IEnumerable<Entity> Entities
{
get => _entities;
set
{
_entities = [.. value];
SelectedEntity = Entities.FirstOrDefault();
OnPropertyChanged();
}
}
private ObservableCollection<Entity> _entities = [];
public EntityListViewModel(
Pf2eDbContext context,
SelectedEntityTypeStore selectedEntityTypeStore,
SelectedEntityStore selectedEntityStore
)
{
Context = context;
SelectedEntityTypeStore = selectedEntityTypeStore;
SelectedEntityStore = selectedEntityStore;
SelectedEntityTypeStore.SelectedEntityTypeChanged +=
SelectedEntityTypeStore_SelectedEntityTypeChanged;
}
~EntityListViewModel()
{
SelectedEntityTypeStore.SelectedEntityTypeChanged -=
SelectedEntityTypeStore_SelectedEntityTypeChanged;
}
private void SelectedEntityTypeStore_SelectedEntityTypeChanged(EntityType? entityType)
{
if (entityType is null)
{
Page = 1;
EntitiesCount = 1;
Entities = [];
return;
}
Page = 1;
Reload();
}
#region "Методы кнопок"
private PageNavigationCommand? _firstPageCommand;
public PageNavigationCommand FirstPageCommand
{
get
{
return _firstPageCommand ??= new PageNavigationCommand(() =>
{
Page = 1;
});
}
}
private PageNavigationCommand? _previousPageCommand;
public PageNavigationCommand PreviousPageCommand
{
get
{
return _previousPageCommand ??= new PageNavigationCommand(() =>
{
Page--;
});
}
}
private PageNavigationCommand? _nextPageCommand;
public PageNavigationCommand NextPageCommand
{
get
{
return _nextPageCommand ??= new PageNavigationCommand(() =>
{
Page++;
});
}
}
private PageNavigationCommand? _lastPageCommand;
public PageNavigationCommand LastPageCommand
{
get
{
return _lastPageCommand ??= new PageNavigationCommand(() =>
{
Page = PageCount;
});
}
}
public bool HasPrevious => Page != 1;
public bool HasNext => Page != PageCount;
#endregion
#region "Запросы"
/// <summary>
/// Выполняет запрос к базе данных и обрабатывает результаты.
/// </summary>
public void Reload()
{
Pf2eDbContext dbContext = new();
IQueryable<Entity> query =
Search == ""
? dbContext.Entities.Where(e => e.Type == SelectedEntityType)
:
// ReSharper disable once EntityFramework.UnsupportedServerSideFunctionCall
dbContext.Entities.Where(e =>
e.Type == SelectedEntityType && Pf2eDbContext.LocaleSearch(e.Name, Search)
);
EntitiesCount = query.Count();
Entities = [.. query.OrderBy(e => e.Id).Skip((Page - 1) * PerPage).Take(PerPage).ToList()];
}
#endregion
}
/// <summary>
/// Команда для навигации по страницам.
/// </summary>
/// <param name="action">Действие, выполняемое при выполнении команды.</param>
public class PageNavigationCommand(Action action) : System.Windows.Input.ICommand
{
/// <summary>
/// Действие, выполняемое при выполнении команды.
/// </summary>
public Action Action { get; } = action;
/// <summary>
/// Событие, вызываемое при изменении возможности выполнения команды.
/// </summary>
public event EventHandler? CanExecuteChanged;
/// <summary>
/// Проверяет, может ли команда быть выполнена с указанным параметром.
/// </summary>
/// <param name="parameter">Параметр команды.</param>
/// <returns>Флаг, указывающий, может ли команда быть выполнена.</returns>
public bool CanExecute(object? parameter)
{
return true;
}
/// <summary>
/// Выполняет команду с указанным параметром.
/// </summary>
/// <param name="parameter">Параметр команды.</param>
public void Execute(object? parameter)
{
Action.Invoke();
}
}
@@ -0,0 +1,153 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Pf2e.Database;
using Pf2e.Scrapper.Helpers;
using Pf2e.Scrapper.WPF.EntityViewer.EntityViewStructure;
using Pf2e.Scrapper.WPF.EntityViewer.Stores;
namespace Pf2e.WPF.EntityViewer.ViewModel;
internal class ViewViewModel : ViewModelBase
{
public Pf2eDbContext Context { get; }
public SelectedEntityStore SelectedEntityStore { get; }
public FullEntity? CurrentEntityView
{
get => _currentEntityView;
set
{
_currentEntityView = value;
OnPropertyChanged();
}
}
private FullEntity? _currentEntityView;
public ObservableCollection<StructureRow> Rows
{
get => _rows;
set
{
_rows = value;
OnPropertyChanged();
}
}
private ObservableCollection<StructureRow> _rows = [];
private readonly List<StructureField> _fields = [];
private readonly List<StructureRow> _allRows = [];
public ViewViewModel(Pf2eDbContext context, SelectedEntityStore selectedEntityStore)
{
Context = context;
SelectedEntityStore = selectedEntityStore;
SelectedEntityStore.SelectedEntityChanged += SelectedEntityStore_SelectedEntityTypeChanged;
}
~ViewViewModel()
{
SelectedEntityStore.SelectedEntityChanged -= SelectedEntityStore_SelectedEntityTypeChanged;
}
private void SelectedEntityStore_SelectedEntityTypeChanged(Database.Model.Base.Entity? entity)
{
if (entity is null)
{
CurrentEntityView = null;
return;
}
CurrentEntityView = new(Context, entity.Type, entity.Id);
if (CurrentEntityView.Entity is null)
{
CurrentEntityView = null;
return;
}
StructureBuilder builder = new();
_fields.Clear();
_fields.Add(builder.Create("Entity", CurrentEntityView.Entity));
if (CurrentEntityView.SubEntity is not null)
{
string typeName = CurrentEntityView.SubEntity.GetType().Name;
_fields.Add(builder.Create(typeName, CurrentEntityView.SubEntity));
}
_allRows.Clear();
_allRows.AddRange(
_fields.SelectMany(field =>
field.GetRows(structureField => new StructureRow(structureField))
)
);
Rows.Clear();
foreach (StructureRow row in _allRows)
{
Rows.Add(row);
row.OnChange += Row_OnChange;
}
BuildLadder();
OnPropertyChanged(nameof(Rows));
}
private void Row_OnChange(StructureRow row, StructureField field)
{
OnPropertyChanged(nameof(Rows));
}
/// <summary>
/// Построение лестницы для строк полей структуры.
/// </summary>
private void BuildLadder()
{
List<LadderType> lastLadder = [];
for (int i = Rows.Count - 1; i >= 0; i--)
{
StructureRow row = Rows[i];
List<LadderType> ladder;
if (row.Depth == 0)
{
ladder = [];
}
else if (row.Depth < lastLadder.Count)
{
ladder =
[
.. lastLadder[..(row.Depth - 1)],
lastLadder[row.Depth - 1] == LadderType.Empty
? LadderType.Last
: LadderType.Through,
];
}
else if (row.Depth == lastLadder.Count)
{
ladder = lastLadder;
}
else
{
ladder =
[
.. lastLadder,
.. Enumerable.Repeat(LadderType.Empty, row.Depth - lastLadder.Count - 1),
LadderType.Last,
];
}
row.Ladder = ladder;
lastLadder = ladder.Select(l => l == LadderType.Last ? LadderType.Through : l).ToList();
}
}
}
+46
View File
@@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<DefaultDocumentationFolder>..\..\Documentation\$(MSBuildProjectName)</DefaultDocumentationFolder>
<DefaultDocumentationFileNameFactory>Name</DefaultDocumentationFileNameFactory>
</PropertyGroup>
<ItemGroup>
<Page Remove="ProcessApplication.xaml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DefaultDocumentation" Version="1.1.0" PrivateAssets="all" />
<PackageReference
Include="Microsoft.Extensions.DependencyInjection.Abstractions"
Version="9.0.5"
/>
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3240.44" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Scrapper\Pf2e.Scrapper.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="EntityViewer\Component\EntityListControl.xaml.cs">
<SubType>Code</SubType>
</Compile>
<Compile Update="EntityViewer\Component\ViewLadder.xaml.cs">
<SubType>Code</SubType>
</Compile>
<Compile Update="EntityViewer\EntityForm.xaml.cs">
<SubType>Code</SubType>
</Compile>
<Compile Update="ProgressForm\ProgressForm.xaml.cs">
<SubType>Code</SubType>
</Compile>
<Compile Update="StartForm\Components\ProcessorCollectionControl.xaml.cs">
<SubType>Code</SubType>
</Compile>
<Compile Update="StartForm\StartForm.xaml.cs">
<SubType>Code</SubType>
</Compile>
</ItemGroup>
</Project>
+6
View File
@@ -0,0 +1,6 @@
<Application
x:Class="Pf2e.WPF.ProcessApplication"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Resources />
</Application>
+8
View File
@@ -0,0 +1,8 @@
using System.Windows;
namespace Pf2e.WPF;
/// <summary>
/// Interaction logic for ProcessApplication.xaml
/// </summary>
public partial class ProcessApplication : Application { }
+53
View File
@@ -0,0 +1,53 @@
using System;
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using Pf2e.Scrapper.Importers.Implementations.Aonprd;
using Pf2e.Scrapper.Importers.Implementations.FoundryEn;
using Pf2e.Scrapper.WPF;
namespace Pf2e.Scrapper;
public static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
public static void Main()
{
// To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize();
// Application.SetHighDpiMode(HighDpiMode.SystemAware);
/*
#pragma warning disable WFO5001
Application.SetColorMode(SystemColorMode.System);
#pragma warning restore WFO5001
*/
IServiceCollection services = RegisterServices();
IServiceProvider serviceProvider = services.BuildServiceProvider();
ImportApplication app = ActivatorUtilities.CreateInstance<ImportApplication>(
serviceProvider
);
app.Run();
// Application.Run(app);
}
private static IServiceCollection RegisterServices()
{
IServiceCollection services = new ServiceCollection();
services.RegisterWpfServices();
ImportApplication.RegisterServices(services);
AonprdImporter.RegisterServices(services);
FoundryEnImporter.RegisterServices(services);
return services;
}
}
+97
View File
@@ -0,0 +1,97 @@
<Window
x:Class="Pf2e.WPF.ProgressForm.ProgressForm"
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"
x:Name="_this"
Title="Выполняется обработка..."
Width="400"
Height="500"
x:ClassModifier="internal"
mc:Ignorable="d">
<Grid Margin="3,3,3,3">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="25" />
<RowDefinition Height="Auto" />
<RowDefinition Height="25" />
<RowDefinition Height="25" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<Label
x:Name="ProcessorsLabel"
Grid.Row="0"
VerticalContentAlignment="Center"
Content="Label" />
<ProgressBar
x:Name="ProcessorsProgressBar"
Grid.Row="1"
Margin="0,0,0,5" />
<Label
x:Name="LocalLabel"
Grid.Row="2"
VerticalContentAlignment="Center"
Content="Label" />
<ProgressBar
x:Name="LocalProgressBar"
Grid.Row="3"
Margin="0,0,0,5" />
<WrapPanel Grid.Row="4" />
<ListView
x:Name="MessageListView"
Grid.Row="5"
ItemsSource="{Binding ViewRows, ElementName=_this}"
ScrollViewer.CanContentScroll="True">
<ListView.Resources>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Foreground" Value="Black" />
<Style.Triggers>
<DataTrigger Binding="{Binding Level}" Value="Progress">
<Setter Property="Background" Value="LightGray" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListView.Resources>
<ListView.View>
<GridView>
<GridViewColumn
Width="Auto"
DisplayMemberBinding="{Binding Time}"
Header="Время" />
<GridViewColumn
Width="Auto"
DisplayMemberBinding="{Binding Level}"
Header="Тип" />
<GridViewColumn DisplayMemberBinding="{Binding Message}" Header="Сообщение" />
</GridView>
</ListView.View>
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock
Grid.Column="0"
HorizontalAlignment="Left"
Background="{Binding Path=Color}" />
<TextBlock
Grid.Column="1"
HorizontalAlignment="Left"
Background="{Binding Path=Color}" />
<TextBlock
Grid.Column="2"
HorizontalAlignment="Left"
Background="{Binding Path=Color}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Window>
@@ -0,0 +1,89 @@
using System;
using System.Collections.ObjectModel;
using System.Drawing;
using System.Windows;
using LinqToDB.Tools;
using Pf2e.Scrapper.Helpers;
namespace Pf2e.WPF.ProgressForm;
/// <summary>
/// Логика взаимодействия для ProgressFormWpf.xaml
/// </summary>
internal partial class ProgressForm : Window
{
public ProcessProgress Progress { get; }
public ObservableCollection<ListViewRow> ViewRows { get; set; } = [];
public ProgressForm(ProcessProgress progress)
{
Progress = progress;
InitializeComponent();
Progress.ProgressUpdate += Progress_ProgressUpdate;
Progress.ProgressMessage += Progress_ProgressMessage;
Progress.ProgressComplete += Progress_ProgressComplete;
}
private void Progress_ProgressUpdate(ProcessProgress sender)
{
if (!Dispatcher.CheckAccess())
{
Dispatcher.Invoke(() => Progress_ProgressUpdate(sender));
return;
}
ProcessorsLabel.Content = sender.Processors.ToString();
ProcessorsProgressBar.Maximum = sender.Processors.Count;
ProcessorsProgressBar.Value = sender.Processors.Current;
LocalLabel.Content = sender.Local.ToString();
LocalProgressBar.Maximum = sender.Local.Count;
LocalProgressBar.Value = sender.Local.Current;
}
private void Progress_ProgressMessage(ProcessProgress sender, LogLevel level, string message)
{
if (!Dispatcher.CheckAccess())
{
Dispatcher.Invoke(() => Progress_ProgressMessage(sender, level, message));
return;
}
ListViewRow item = new(level, message) { IsVisible = true };
ViewRows.Add(item);
MessageListView.ScrollIntoView(item);
}
private void Progress_ProgressComplete(ProcessProgress sender)
{
ApplicationStateChange?.Invoke(this, ApplicationStage.Viewer);
}
public event ImportApplication.ApplicationStateChangeDelegate? ApplicationStateChange;
}
internal class ListViewRow(LogLevel level, string message)
{
public string Time { get; } = DateTime.Now.ToString("HH:mm:ss.fff");
public LogLevel Level { get; } = level;
public string Message { get; } = message;
public Color Color { get; } =
level switch
{
LogLevel.Important => Color.Salmon,
LogLevel.Warning => Color.Khaki,
LogLevel.Information => Color.White,
LogLevel.Progress => Color.LightGray,
_ => Color.White,
};
public bool IsVisible { get; set; } = true;
}
@@ -0,0 +1,414 @@
<UserControl
x:Class="Pf2e.Scrapper.WPF.StartForm.Components.ProcessorCollectionControl"
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"
d:DesignHeight="400"
d:DesignWidth="300"
x:ClassModifier="internal"
BorderThickness="2,2,2,2"
mc:Ignorable="d">
<UserControl.Resources>
<Style x:Key="HeaderCheckbox" TargetType="{x:Type CheckBox}">
<Setter Property="Margin" Value="0,0,0,0" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style x:Key="ElementBorder" TargetType="{x:Type Border}">
<Setter Property="Margin" Value="10,5,10,20" />
<Setter Property="BorderThickness" Value="1,1,1,1" />
<Setter Property="BorderBrush" Value="Black" />
</Style>
<Style x:Key="ElementPanelStyle" TargetType="{x:Type WrapPanel}">
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled" />
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="{DynamicResource {x:Static SystemColors.ControlLightLightColorKey}}" />
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ElementCheckBox" TargetType="CheckBox">
<Setter Property="Margin" Value="4,2,4,2" />
</Style>
</UserControl.Resources>
<UserControl.Background>
<SolidColorBrush Color="{DynamicResource {x:Static SystemColors.ControlColorKey}}" />
</UserControl.Background>
<ScrollViewer>
<StackPanel
x:Name="RootPanel"
Margin="3,3,3,3"
ScrollViewer.CanContentScroll="True">
<CheckBox
x:Name="Test"
Content="Test Header 1"
IsChecked="False"
Style="{DynamicResource HeaderCheckbox}" />
<Border Style="{DynamicResource ElementBorder}">
<WrapPanel Style="{StaticResource ElementPanelStyle}">
<CheckBox
Content="Test 1"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 2"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 3"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 4"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
</WrapPanel>
</Border>
<CheckBox
Content="Test Header 2"
IsChecked="False"
Style="{DynamicResource HeaderCheckbox}" />
<Border Style="{DynamicResource ElementBorder}">
<WrapPanel Style="{StaticResource ElementPanelStyle}">
<CheckBox
Content="Test 1"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 2 Test 2 Test 2"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 3"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 4"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 1"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 2"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 3"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 4"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 1"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 2"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 3"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 4"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
</WrapPanel>
</Border>
<CheckBox
Content="Test Header 2"
IsChecked="False"
Style="{DynamicResource HeaderCheckbox}" />
<Border Style="{DynamicResource ElementBorder}">
<WrapPanel Style="{StaticResource ElementPanelStyle}">
<CheckBox
Content="Test 1"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 2 Test 2 Test 2"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 3"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 4"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 1"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 2"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 3"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 4"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 1"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 2"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 3"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 4"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
</WrapPanel>
</Border>
<CheckBox
Content="Test Header 2"
IsChecked="False"
Style="{DynamicResource HeaderCheckbox}" />
<Border Style="{DynamicResource ElementBorder}">
<WrapPanel Style="{StaticResource ElementPanelStyle}">
<CheckBox
Content="Test 1"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 2 Test 2 Test 2"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 3"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 4"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 1"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 2"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 3"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 4"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 1"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 2"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 3"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 4"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
</WrapPanel>
</Border>
<CheckBox
Content="Test Header 2"
IsChecked="False"
Style="{DynamicResource HeaderCheckbox}" />
<Border Style="{DynamicResource ElementBorder}">
<WrapPanel Style="{StaticResource ElementPanelStyle}">
<CheckBox
Content="Test 1"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 2 Test 2 Test 2"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 3"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 4"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 1"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 2"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 3"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 4"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 1"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 2"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 3"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 4"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
</WrapPanel>
</Border>
<CheckBox
Content="Test Header 2"
IsChecked="False"
Style="{DynamicResource HeaderCheckbox}" />
<Border Style="{DynamicResource ElementBorder}">
<WrapPanel Style="{StaticResource ElementPanelStyle}">
<CheckBox
Content="Test 1"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 2 Test 2 Test 2"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 3"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 4"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 1"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 2"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 3"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 4"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 1"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 2"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 3"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 4"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
</WrapPanel>
</Border>
<CheckBox
Content="Test Header 2"
IsChecked="False"
Style="{DynamicResource HeaderCheckbox}" />
<Border Style="{DynamicResource ElementBorder}">
<WrapPanel Style="{StaticResource ElementPanelStyle}">
<CheckBox
Content="Test 1"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 2 Test 2 Test 2"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 3"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 4"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 1"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 2"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 3"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 4"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 1"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 2"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 3"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
<CheckBox
Content="Test 4"
IsChecked="False"
Style="{DynamicResource ElementCheckBox}" />
</WrapPanel>
</Border>
</StackPanel>
</ScrollViewer>
</UserControl>
@@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using Pf2e.Scrapper.Processors.Attributes;
using Pf2e.Scrapper.Processors.Collection;
using CheckBox = System.Windows.Controls.CheckBox;
namespace Pf2e.Scrapper.WPF.StartForm.Components;
/// <summary>
/// Логика взаимодействия для ProcessorCollectionControl.xaml
/// </summary>
internal partial class ProcessorCollectionControl : System.Windows.Controls.UserControl
{
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public ProcessorCollection Collection
{
get => _collection;
set
{
_collection = value;
SetContent();
}
}
private ProcessorCollection _collection = null!;
public ProcessorCollectionControl()
{
InitializeComponent();
}
private Dictionary<ProcessorGroupEnum, CheckBox> GroupCheckBoxes { get; } = [];
private Dictionary<ProcessorGroupEnum, bool?> GroupLastStats { get; } = [];
private Dictionary<string, CheckBox> ItemCheckBoxes { get; } = [];
private void SetContent()
{
RootPanel.Children.Clear();
foreach (ProcessorGroupEnum group in Enum.GetValues<ProcessorGroupEnum>())
{
GroupCheckBoxes[group] = new()
{
Content = group.ToString(),
IsThreeState = true,
Style = (Style)Resources["HeaderCheckbox"]!,
};
GroupCheckBoxes[group].Click += GroupCheckBox_Click;
System.Windows.Controls.Border border = new()
{
Style = (Style)Resources["ElementBorder"]!,
};
System.Windows.Controls.WrapPanel wrapPanel = new()
{
Style = (Style)Resources["ElementPanelStyle"]!,
};
foreach (ProcessorRecord item in Collection.AllItems.Where(item => item.Group == group))
{
ItemCheckBoxes[item.Name] = new()
{
Content = item.Description,
ToolTip = item.Name,
IsChecked = item.Active,
Style = (Style)Resources["ElementCheckBox"]!,
};
ItemCheckBoxes[item.Name].Click += ItemCheckBox_Click;
wrapPanel.Children.Add(ItemCheckBoxes[item.Name]);
}
border.Child = wrapPanel;
RootPanel.Children.Add(GroupCheckBoxes[group]);
RootPanel.Children.Add(border);
}
UpdateContent();
}
private void UpdateContent()
{
GroupLastStats.Clear();
List<string> checkedItems = [];
foreach ((string key, CheckBox? checkBox) in ItemCheckBoxes)
{
ProcessorRecord item = Collection[key];
bool isChecked = checkBox.IsChecked ?? false;
if (GroupLastStats.TryGetValue(item.Group, out bool? state))
{
/*
* | true | false | null
* | Checked | Unchecked | Indeterminate
* -------+---------+-----------+---------------
* true | true | null | null
* false | null | false | null
*/
GroupLastStats[item.Group] =
isChecked && state == true ? true
: !isChecked && state == false ? false
: null;
}
else
{
GroupLastStats[item.Group] = isChecked;
}
if (isChecked)
{
checkedItems.Add(key);
}
}
foreach ((ProcessorGroupEnum groupEnum, CheckBox? checkBox) in GroupCheckBoxes)
{
checkBox.IsChecked = GroupLastStats.GetValueOrDefault(groupEnum, null);
}
Collection.FilterProcessors(checkedItems);
}
private void GroupCheckBox_Click(object sender, RoutedEventArgs e)
{
CheckBox groupCheckBox = (CheckBox)sender;
ProcessorGroupEnum groupEnum = GroupCheckBoxes
.First(pair => pair.Value == groupCheckBox)
.Key;
groupCheckBox.IsChecked =
GroupLastStats[groupEnum] is null || groupCheckBox.IsChecked is not null;
foreach (
(string _, CheckBox checkBox) in ItemCheckBoxes.Where(pair =>
Collection[pair.Key].Group == groupEnum
)
)
{
checkBox.IsChecked = groupCheckBox.IsChecked;
}
UpdateContent();
}
private void ItemCheckBox_Click(object sender, RoutedEventArgs e)
{
UpdateContent();
}
}
+61
View File
@@ -0,0 +1,61 @@
<Window
x:Class="Pf2e.WPF.StartForm.StartForm"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:comp="clr-namespace:Pf2e.WPF.StartForm.Components"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="StartForm"
Width="800"
Height="600"
x:ClassModifier="internal"
Loaded="Window_Loaded"
mc:Ignorable="d">
<Window.Background>
<SolidColorBrush Color="{DynamicResource {x:Static SystemColors.ControlColorKey}}" />
</Window.Background>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="50" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" MinWidth="100" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="5*" MinWidth="200" />
</Grid.ColumnDefinitions>
<StackPanel
Grid.Row="0"
Grid.Column="0"
Margin="3,3,3,3">
<Button
Margin="0,0,0,3"
Click="ViewerButton_Click"
Content="Просмотр сущностей" />
<Button Content="Просмотр правил" />
</StackPanel>
<Button
Grid.Row="1"
Grid.Column="0"
Margin="3,3,3,3"
Click="StartButton_Click"
Content="Старт" />
<GridSplitter
Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="1"
Width="5"
HorizontalAlignment="Stretch" />
<comp:ProcessorCollectionControl
x:Name="ProcessorCollectionControl"
Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="2" />
</Grid>
</Window>
+61
View File
@@ -0,0 +1,61 @@
using System.Windows;
using Pf2e.Scrapper;
using Pf2e.Scrapper.Processors.Collection;
namespace Pf2e.WPF.StartForm;
/// <summary>
/// Класс начальной формы приложения.
/// </summary>
internal partial class StartForm : Window
{
/// <summary>
/// Коллекция обработчиков.
/// </summary>
public ProcessorCollection Collection { get; }
/// <summary>
/// Конструктор класса <see cref="StartForm"/>.
/// </summary>
/// <param name="collection">[DI] Коллекция обработчиков.</param>
public StartForm(ProcessorCollection collection)
{
Collection = collection;
InitializeComponent();
}
/// <summary>
/// Событие загрузки формы.
/// </summary>
/// <param name="sender">Отправитель события.</param>
/// <param name="e">Аргументы события.</param>
private void Window_Loaded(object sender, RoutedEventArgs e)
{
ProcessorCollectionControl.Collection = Collection;
}
/// <summary>
/// Событие нажатия на кнопку просмотра сущностей.
/// </summary>
/// <param name="sender">Отправитель события.</param>
/// <param name="e">Аргументы события.</param>
private void ViewerButton_Click(object sender, RoutedEventArgs e)
{
ApplicationStateChange?.Invoke(this, ApplicationStage.Viewer);
}
/// <summary>
/// Событие нажатия кнопки начала процесса импорта.
/// </summary>
/// <param name="sender">Отправитель события.</param>
/// <param name="e">Аргументы события.</param>
private void StartButton_Click(object sender, RoutedEventArgs e)
{
ApplicationStateChange?.Invoke(this, ApplicationStage.ImportProcess);
}
/// <summary>
/// Событие изменения состояния приложения.
/// </summary>
public event ImportApplication.ApplicationStateChangeDelegate? ApplicationStateChange;
}
+41
View File
@@ -0,0 +1,41 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Pf2e.WPF;
/// <summary>
/// Базовый класс для ViewModel, реализующий INotifyPropertyChanged.
/// </summary>
internal abstract class ViewModelBase : INotifyPropertyChanged
{
/// <summary>
/// Событие, возникающее при изменении свойства.
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;
/// <summary>
/// Вызывает событие PropertyChanged для указанного свойства.
/// </summary>
/// <param name="propertyName">Название свойства (заполняется автоматически).</param>
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new(propertyName));
}
/// <summary>
/// Устанавливает значение поля и вызывает событие PropertyChanged, если значение изменилось.
/// </summary>
/// <typeparam name="T">Тип поля.</typeparam>
/// <param name="field">Поле, которое нужно установить.</param>
/// <param name="value">Значение, которое нужно установить.</param>
/// <param name="propertyName">Название свойства (заполняется автоматически).</param>
/// <returns>Истина, если значение изменилось, иначе ложь.</returns>
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
+30
View File
@@ -0,0 +1,30 @@
using Microsoft.Extensions.DependencyInjection;
using Pf2e.Scrapper.WPF.EntityViewer.Stores;
using Pf2e.Scrapper.WPF.EntityViewer.ViewModel;
namespace Pf2e.WPF;
/// <summary>
/// Расширение для IServiceCollection, позволяющее регистрировать сервисы WPF.
/// </summary>
public static class WpfServiceProviderExtension
{
/// <summary>
/// Регистрация сервисов WPF в IServiceCollection.
/// </summary>
/// <param name="collection">Коллекция сервисов.</param>
/// <returns>Коллекция сервисов (<paramref name="collection"/> для Fluent API).</returns>
public static IServiceCollection RegisterWpfServices(this IServiceCollection collection)
{
return collection
// ViewModels
.AddTransient<EntityFormViewModel>()
.AddTransient<EntityListViewModel>()
.AddTransient<ViewViewModel>()
// Stores
.AddSingleton<SelectedEntityTypeStore>()
.AddSingleton<SelectedEntityStore>()
//
;
}
}