viewmodels, uis, ecc.

This commit is contained in:
lanyi 2020-03-28 23:16:45 +01:00
parent bda455b7e3
commit 1d706b1adc
18 changed files with 1040 additions and 194 deletions

View File

@ -12,6 +12,9 @@ namespace HashCalculator.GUI
public IEnumerable<string> DisplayLabels { get; } public IEnumerable<string> DisplayLabels { get; }
public uint Hash => SageHash.CalculateLowercaseHash(Name); public uint Hash => SageHash.CalculateLowercaseHash(Name);
public string IdString => $"{Type}:{Name}";
public string HashString => $"{Hash:0X} ({Hash})";
public AssetEntry(XElement element) public AssetEntry(XElement element)
{ {
if (element == null) if (element == null)
@ -26,12 +29,7 @@ namespace HashCalculator.GUI
Type = element.Name.LocalName; Type = element.Name.LocalName;
var id = element.Attribute("id")?.Value; var id = element.Attribute("id")?.Value;
if (id == null) Name = id ?? throw new NotSupportedException();
{
throw new NotSupportedException();
}
Name = id;
var labels = from name in element.Elements(ModXml.EalaAsset + "DisplayName") var labels = from name in element.Elements(ModXml.EalaAsset + "DisplayName")
select name.Value; select name.Value;

131
Command.cs Normal file
View File

@ -0,0 +1,131 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
namespace HashCalculator.GUI
{
internal class Command<T> : ICommand
{
private readonly Func<T, Task> _action;
private readonly Action<Exception> _onAsyncException;
private bool _canExecute = true;
public bool CanExecuteValue
{
get => _canExecute;
set
{
_canExecute = value;
CanExecuteChanged?.Invoke(this, new EventArgs());
}
}
public event EventHandler? CanExecuteChanged;
public Command(Func<T, Task> action, Action<Exception>? onAsyncException = null)
{
_action = action;
_onAsyncException = onAsyncException ?? (exception =>
{
MessageBox.Show($"Unhandled async exception in {nameof(Command<T>)}: {exception}");
Application.Current.Shutdown(1);
});
}
public bool CanExecute(object parameter)
{
return _canExecute;
}
[SuppressMessage("Globalization", "CA1303:请不要将文本作为本地化参数传递", Justification = "<挂起>")]
public void Execute(object parameter)
{
if (!_canExecute)
{
return;
}
if(!(parameter is T typed))
{
throw new ArgumentException($"{nameof(parameter)} wrong type");
}
ExecuteTaskInternal(ExecuteTask(typed));
}
public Task ExecuteTask(T parameter) => _action(parameter);
[SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")]
private async void ExecuteTaskInternal(Task task)
{
try
{
await task.ConfigureAwait(true);
}
catch (Exception exception)
{
_onAsyncException(exception);
}
}
}
internal class Command : ICommand
{
private readonly Func<Task> _action;
private readonly Action<Exception> _onAsyncException;
private bool _canExecute = true;
public bool CanExecuteValue
{
get => _canExecute;
set
{
_canExecute = value;
CanExecuteChanged?.Invoke(this, new EventArgs());
}
}
public event EventHandler? CanExecuteChanged;
public Command(Func<Task> action, Action<Exception>? onAsyncException = null)
{
_action = action;
_onAsyncException = onAsyncException ?? (exception =>
{
MessageBox.Show($"Unhandled async exception in {nameof(Command)}: {exception}");
Application.Current.Shutdown(1);
});
}
public bool CanExecute(object parameter)
{
return _canExecute;
}
public void Execute(object parameter)
{
if (!_canExecute)
{
return;
}
ExecuteTaskInternal(ExecuteTask());
}
public Task ExecuteTask() => _action();
[SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")]
private async void ExecuteTaskInternal(Task task)
{
try
{
await task.ConfigureAwait(true);
}
catch (Exception exception)
{
_onAsyncException(exception);
}
}
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace HashCalculator.GUI.Converters
{
[ValueConversion(typeof(bool), typeof(bool))]
internal class BooleanInvertConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
bool original = (bool)value;
return !original;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
bool original = (bool)value;
return !original;
}
}
}

View File

@ -0,0 +1,28 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace HashCalculator.GUI.Converters
{
[ValueConversion(typeof(object), typeof(bool))]
public class NullToBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if(value is null)
{
return false;
}
else if(value is string s)
{
return !string.IsNullOrEmpty(s);
}
return true;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace HashCalculator.GUI.Converters
{
[ValueConversion(typeof(InputEntry), typeof(bool))]
internal class ValidInputEntryTypeToBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var entry = value as InputEntry;
return entry?.IsValid == true;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -1,17 +1,16 @@
using System; using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Windows; using System.Linq;
using System.Windows.Data; using System.Windows.Data;
namespace HashCalculator.GUI namespace HashCalculator.GUI.Converters
{ {
[ValueConversion(typeof(string), typeof(Visibility))] internal class ValueConverterAggregate : List<IValueConverter>, IValueConverter
public class NullToVisibilityConverter : IValueConverter
{ {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{ {
var text = value as string; return this.Aggregate(value, (current, converter) => converter.Convert(current, targetType, parameter, culture));
return string.IsNullOrEmpty(text) ? Visibility.Visible : Visibility.Collapsed;
} }
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)

50
CopyableBox.xaml Normal file
View File

@ -0,0 +1,50 @@
<Window
x:Class="HashCalculator.GUI.CopyableBox"
x:Name="Self"
x:ClassModifier="internal"
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:local="clr-namespace:HashCalculator.GUI"
mc:Ignorable="d"
Title="CopyableBox" Height="350" Width="600"
Closing="ClosingHandler"
>
<Window.DataContext>
<local:CopyableBoxViewModel />
</Window.DataContext>
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="60"/>
</Grid.RowDefinitions>
<Button
Content="复制所有内容"
Command="{Binding CopyCommand}"
Grid.Row="1"
Margin="10,0,0,10"
Width="120"
Height="30"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
/>
<Button
Content="{Binding CloseButtonText}"
Command="{Binding CloseCommand}"
CommandParameter="{Binding ElementName=Self}"
Margin="0,0,10,10"
Grid.Row="1"
Width="100"
Height="30"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
/>
<TextBox
Grid.Row="0"
Text="{Binding Text, Mode=OneWay}"
IsReadOnly="True"
TextWrapping="Wrap"
/>
</Grid>
</Window>

166
CopyableBox.xaml.cs Normal file
View File

@ -0,0 +1,166 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Threading;
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.Shapes;
using System.Windows.Threading;
using TechnologyAssembler.Core.Extensions;
namespace HashCalculator.GUI
{
/// <summary>
/// CopyableBox.xaml 的交互逻辑
/// </summary>
[SuppressMessage("Microsoft.Performance", "CA1812")]
internal partial class CopyableBox : Window, IDisposable
{
private CopyableBoxViewModel ViewModel => (CopyableBoxViewModel)DataContext;
public static void ShowDialog(Func<CancellationToken, Task<string>> action)
{
using var box = new CopyableBox();
box.ViewModel.Initialize(action, box.Dispatcher);
box.ShowDialog();
}
private CopyableBox()
{
InitializeComponent();
}
public void Dispose()
{
ViewModel.Dispose();
}
private void ClosingHandler(object sender, CancelEventArgs e)
{
if (ViewModel.ReadyToClose)
{
return;
}
e.Cancel = true;
if (ViewModel.CloseCommand.CanExecuteValue)
{
ViewModel.CloseCommand.Execute(this);
}
return;
}
}
[SuppressMessage("Microsoft.Performance", "CA1812")]
internal class CopyableBoxViewModel : NotifyPropertyChanged, IDisposable
{
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private Task? _task;
public const string InitialMessage = "正在加载…… 关闭窗口就可以取消加载(";
private string _text = InitialMessage;
public string Text
{
get => _text;
private set => SetField(ref _text, value);
}
public string CloseButtonText => CloseCommand.CanExecuteValue ? "关闭窗口" : "正在关闭…";
private bool _readyToClose = false;
public bool ReadyToClose
{
get => _readyToClose;
private set => SetField(ref _readyToClose, value);
}
public Command CopyCommand { get; }
public Command<CopyableBox> CloseCommand { get; }
public CopyableBoxViewModel()
{
CopyCommand = new Command(() =>
{
Clipboard.SetText(Text);
return Task.CompletedTask;
});
CloseCommand = new Command<CopyableBox>(async window =>
{
CloseCommand.CanExecuteValue = false;
Notify(nameof(CloseButtonText));
if (_task != null)
{
try
{
Cancel();
await _task.ConfigureAwait(true);
}
catch (OperationCanceledException) { }
}
ReadyToClose = true;
window.Close();
});
}
public void Dispose()
{
Cancel();
_cancellationTokenSource.Dispose();
}
public void Initialize(Func<CancellationToken, Task<string>> action, Dispatcher dispatcher)
{
_task = InitializationTask(action, dispatcher);
}
private async Task InitializationTask(Func<CancellationToken, Task<string>> action, Dispatcher dispatcher)
{
var utcBegin = DateTimeOffset.UtcNow;
var timer = new DispatcherTimer(TimeSpan.FromMilliseconds(200), DispatcherPriority.Normal, (s, e) =>
{
var timeElapsed = DateTimeOffset.UtcNow - utcBegin;
if (((DispatcherTimer)s).IsEnabled && timeElapsed.TotalSeconds > 1)
{
Text = Text = $"{InitialMessage}\r\n目前耗时{timeElapsed}稍微再等一下吧233";
}
}, dispatcher);
timer.Start();
var token = _cancellationTokenSource.Token;
async Task<string> Action()
{
try
{
return await action(token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
return "操作已被取消";
}
finally
{
timer.Stop();
}
}
Text = await dispatcher.Invoke(Action).ConfigureAwait(true);
}
private void Cancel()
{
_cancellationTokenSource.Cancel();
}
}
}

View File

@ -30,6 +30,11 @@ namespace HashCalculator.GUI
path = Environment.ExpandEnvironmentVariables(path); path = Environment.ExpandEnvironmentVariables(path);
if (path.IndexOfAny(Path.GetInvalidPathChars()) != -1)
{
return empty;
}
_search.Update(path); _search.Update(path);
if (!_search.IsValidPath) if (!_search.IsValidPath)
@ -37,7 +42,7 @@ namespace HashCalculator.GUI
return empty; return empty;
} }
var currentFiles = empty; var currentFiles = new List<InputEntry>();
string? fileName; string? fileName;
string? currentFullPath; string? currentFullPath;
try try
@ -46,13 +51,12 @@ namespace HashCalculator.GUI
fileName = Path.GetFileName(path); fileName = Path.GetFileName(path);
if (File.Exists(currentFullPath)) if (File.Exists(currentFullPath))
{ {
var type = CheckExtension(currentFullPath); var type = CheckExtension(currentFullPath);
if (type.HasValue) if(type is InputEntryType entryType)
{ {
currentFiles.Append(new InputEntry(type.Value, path, currentFullPath)); currentFiles.Add(new InputEntry(entryType, path, currentFullPath));
} }
currentFiles.Append(new InputEntry(InputEntryType.BinaryFile, path, currentFullPath)); currentFiles.Add(new InputEntry(InputEntryType.BinaryFile, path, currentFullPath));
} }
} }
catch catch
@ -60,6 +64,9 @@ namespace HashCalculator.GUI
return empty; return empty;
} }
var alternatives = empty;
try
{
var directories = from directory in _search.AllDirectories var directories = from directory in _search.AllDirectories
where directory.Name.StartsWith(fileName, StringComparison.OrdinalIgnoreCase) where directory.Name.StartsWith(fileName, StringComparison.OrdinalIgnoreCase)
select new InputEntry(InputEntryType.Path, _search.GetInputStyleName(directory), directory.FullName); select new InputEntry(InputEntryType.Path, _search.GetInputStyleName(directory), directory.FullName);
@ -69,25 +76,19 @@ namespace HashCalculator.GUI
where file.FullName != currentFullPath where file.FullName != currentFullPath
select file; select file;
var supportedFiles = empty; var supportedFiles = from file in otherFiles
try
{
supportedFiles = from file in otherFiles
let type = CheckExtension(file.Extension) let type = CheckExtension(file.Extension)
where type.HasValue where type.HasValue
select new InputEntry(type.Value, _search.GetInputStyleName(file), file.FullName); select new InputEntry(type.Value, _search.GetInputStyleName(file), file.FullName);
}
catch { }
var binaryFiles = empty; var binaryFiles = from file in otherFiles
try
{
binaryFiles = from file in otherFiles
select new InputEntry(InputEntryType.BinaryFile, _search.GetInputStyleName(file), file.FullName); select new InputEntry(InputEntryType.BinaryFile, _search.GetInputStyleName(file), file.FullName);
alternatives = supportedFiles.Concat(directories).Concat(binaryFiles).ToArray();
} }
catch { } catch { }
return currentFiles.Concat(supportedFiles).Concat(directories).Concat(binaryFiles); return currentFiles.Concat(alternatives);
} }
private static InputEntryType? CheckExtension(string path) private static InputEntryType? CheckExtension(string path)
@ -130,6 +131,12 @@ namespace HashCalculator.GUI
string? newBaseDirectory = null; string? newBaseDirectory = null;
try try
{ {
if (Path.GetFileName(path).IndexOfAny(Path.GetInvalidFileNameChars()) != -1)
{
_inputBaseDirectory = null;
return;
}
newBaseDirectory = Path.GetDirectoryName(path); newBaseDirectory = Path.GetDirectoryName(path);
var rootDirectory = Path.GetPathRoot(path); var rootDirectory = Path.GetPathRoot(path);
if(string.IsNullOrEmpty(newBaseDirectory) && !string.IsNullOrEmpty(rootDirectory)) if(string.IsNullOrEmpty(newBaseDirectory) && !string.IsNullOrEmpty(rootDirectory))

View File

@ -21,6 +21,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System.Windows.Presentation" />
<Reference Include="TechnologyAssembler.Core"> <Reference Include="TechnologyAssembler.Core">
<HintPath>TechnologyAssembler.Core.dll</HintPath> <HintPath>TechnologyAssembler.Core.dll</HintPath>
<Private>true</Private> <Private>true</Private>

View File

@ -5,13 +5,18 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:HashCalculator.GUI" xmlns:local="clr-namespace:HashCalculator.GUI"
xmlns:c="clr-namespace:HashCalculator.GUI.Converters"
mc:Ignorable="d" mc:Ignorable="d"
x:ClassModifier="internal" x:ClassModifier="internal"
x:Name="_this" x:Name="_this"
d:DesignHeight="100" d:DesignWidth="800" d:DesignHeight="100" d:DesignWidth="800"
> >
<UserControl.Resources> <UserControl.Resources>
<local:NullToVisibilityConverter x:Key="NullToVisibilityConverter" /> <c:ValueConverterAggregate x:Key="NullToVisibilityConverter">
<c:NullToBooleanConverter />
<c:BooleanInvertConverter />
<BooleanToVisibilityConverter />
</c:ValueConverterAggregate>
<Style <Style
x:Key="BlankListBoxContainerStyle" x:Key="BlankListBoxContainerStyle"
TargetType="{x:Type ListBoxItem}" TargetType="{x:Type ListBoxItem}"
@ -72,7 +77,6 @@
PreviewKeyDown="OnPreviewKeyDown" PreviewKeyDown="OnPreviewKeyDown"
Padding="2, 0" Padding="2, 0"
VerticalContentAlignment="Center" VerticalContentAlignment="Center"
TextChanged="OnTextChanged"
LostFocus="OnLostFocus" LostFocus="OnLostFocus"
/> />
<TextBlock <TextBlock
@ -98,14 +102,28 @@
RelativeSource={RelativeSource AncestorType=local:InputBar}}" RelativeSource={RelativeSource AncestorType=local:InputBar}}"
SelectionMode="Single" SelectionMode="Single"
SelectionChanged="OnSelectionChanged" SelectionChanged="OnSelectionChanged"
SelectedItem="{Binding Path=SelectedItem,
RelativeSource={RelativeSource AncestorType=local:InputBar}}"
SelectedIndex="{Binding Path=SelectedIndex,
RelativeSource={RelativeSource AncestorType=local:InputBar}}"
VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling" VirtualizingPanel.VirtualizationMode="Recycling"
> >
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Text}" FontWeight="Bold"></TextBlock> <TextBlock Text="{Binding Text}"></TextBlock>
<StackPanel
Orientation="Horizontal"
>
<StackPanel.Resources>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="Gray" />
</Style>
</StackPanel.Resources>
<TextBlock Text="{Binding Type}"></TextBlock> <TextBlock Text="{Binding Type}"></TextBlock>
<TextBlock Text="{Binding Details}" Margin="10,0,0,0"></TextBlock>
</StackPanel>
</StackPanel> </StackPanel>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>

View File

@ -82,7 +82,22 @@ namespace HashCalculator.GUI
set => SetValue(SelectedItemProperty, value); set => SetValue(SelectedItemProperty, value);
} }
public event TextChangedEventHandler? TextChanged; public static readonly DependencyProperty SelectedIndexProperty =
DependencyProperty.Register(nameof(SelectedIndex), typeof(int), typeof(InputBar), new FrameworkPropertyMetadata
{
BindsTwoWayByDefault = true,
DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
CoerceValueCallback = (d, baseObject) =>
{
var self = (InputBar)d;
return self.NormalizeIndex((int)baseObject);
}
});
public int SelectedIndex
{
get => (int)GetValue(SelectedIndexProperty);
set => SetValue(SelectedIndexProperty, value);
}
public InputBar() public InputBar()
{ {
@ -94,12 +109,12 @@ namespace HashCalculator.GUI
switch (e.Key) switch (e.Key)
{ {
case Key.Down: case Key.Down:
ListBox.SelectedIndex = NormalizeIndex(ListBox.SelectedIndex + 1); SelectedIndex += 1;
ListBox.ScrollIntoView(ListBox.SelectedItem); ListBox.ScrollIntoView(ListBox.SelectedItem);
DropDown.IsOpen = true; DropDown.IsOpen = true;
break; break;
case Key.Up: case Key.Up:
ListBox.SelectedIndex = NormalizeIndex(ListBox.SelectedIndex - 1); SelectedIndex -= 1;
ListBox.ScrollIntoView(ListBox.SelectedItem); ListBox.ScrollIntoView(ListBox.SelectedItem);
break; break;
case Key.Escape: case Key.Escape:
@ -114,7 +129,6 @@ namespace HashCalculator.GUI
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{ {
SelectedItem = (InputEntry)ListBox.SelectedItem;
if (SelectedItem != null) if (SelectedItem != null)
{ {
Text = SelectedItem.ToString(); Text = SelectedItem.ToString();
@ -122,14 +136,6 @@ namespace HashCalculator.GUI
} }
} }
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
if (Text != SelectedItem?.ToString())
{
TextChanged?.Invoke(this, e);
}
}
private void OnLostFocus(object sender, RoutedEventArgs e) private void OnLostFocus(object sender, RoutedEventArgs e)
{ {
DropDown.IsOpen = false; DropDown.IsOpen = false;
@ -145,6 +151,5 @@ namespace HashCalculator.GUI
return Math.Min(Math.Max(rawIndex, -1), Collection.Count() - 1); return Math.Min(Math.Max(rawIndex, -1), Collection.Count() - 1);
} }
} }
} }

View File

@ -19,9 +19,34 @@ namespace HashCalculator.GUI
internal sealed class InputEntry internal sealed class InputEntry
{ {
public static IReadOnlyDictionary<InputEntryType, string> Descriptions = new Dictionary<InputEntryType, string>
{
{ InputEntryType.BigFile, "可以尝试读取这个 big 文件里的 manifest 文件" },
{ InputEntryType.BinaryFile, "可以计算这个文件的哈希值" },
{ InputEntryType.ManifestFile, "可以尝试读取这个 manifest 文件,显示各个素材的哈希值" },
{ InputEntryType.XmlFile, "可以尝试读取这个XML显示 XML 里定义的各个素材的哈希值" },
{ InputEntryType.Path, string.Empty },
};
public InputEntryType Type { get; } public InputEntryType Type { get; }
public string Value { get; } public string Value { get; }
public string Text { get; } public string Text { get; }
public string Details
{
get
{
if(Type == InputEntryType.Text)
{
var hash = SageHash.CalculateLowercaseHash(Value);
var binaryHash = SageHash.CalculateBinaryHash(Value);
return hash == binaryHash
? $"这段文字的哈希值:{hash:x8} ({hash})"
: $"这段文字的哈希值:{hash:x8};大小写敏感哈希值 {binaryHash:x8}";
}
return Descriptions[Type];
}
}
public bool IsValid => Type != InputEntryType.Text && Type != InputEntryType.Path;
public InputEntry(InputEntryType type, string text, string value) public InputEntry(InputEntryType type, string text, string value)
{ {
@ -34,6 +59,5 @@ namespace HashCalculator.GUI
{ {
return Text; return Text;
} }
} }
} }

View File

@ -1,71 +1,228 @@
<Window x:Class="HashCalculator.GUI.MainWindow" <Window
x:Class="HashCalculator.GUI.MainWindow"
x:Name="Self"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:l="clr-namespace:HashCalculator.GUI" xmlns:l="clr-namespace:HashCalculator.GUI"
xmlns:c="clr-namespace:HashCalculator.GUI.Converters"
mc:Ignorable="d" mc:Ignorable="d"
Title="Sage FastHash 哈希计算器" Height="600" Width="600"> Title="Sage FastHash 哈希计算器" Height="600" Width="600"
Background="#202020"
Foreground="LightGray"
>
<Window.Resources> <Window.Resources>
<l:NullToVisibilityConverter x:Key="NullToVisibilityConverter" /> <c:ValueConverterAggregate x:Key="ValidInputEntryToVisibilityConverter">
<c:ValidInputEntryTypeToBooleanConverter />
<BooleanToVisibilityConverter />
</c:ValueConverterAggregate>
<c:ValueConverterAggregate x:Key="InvalidInputEntryToVisibilityConverter">
<c:ValidInputEntryTypeToBooleanConverter />
<c:BooleanInvertConverter />
<BooleanToVisibilityConverter />
</c:ValueConverterAggregate>
<Style x:Key="CommonStyle" TargetType="Control">
<Setter
Property="Foreground"
Value="{Binding Path=(TextElement.Foreground),
RelativeSource={RelativeSource AncestorType={x:Type FrameworkElement}}}"
/>
<Setter
Property="Background"
Value="{Binding Path=(TextElement.Background),
RelativeSource={RelativeSource AncestorType={x:Type FrameworkElement}}}"
/>
</Style>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="{x:Type TextBox}"
>
<Setter Property="Background" Value="#00000000"/>
</Style>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="{x:Type Button}"
>
<Setter Property="Background" Value="#20808080"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
<ContentPresenter x:Name="contentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" ContentStringFormat="{TemplateBinding ContentStringFormat}" Focusable="False" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="Button.IsDefaulted" Value="True">
<Setter Property="BorderBrush" TargetName="border" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" TargetName="border" Value="#80BEE6FD"/>
<Setter Property="BorderBrush" TargetName="border" Value="#FF3C7FB1"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" TargetName="border" Value="#80C4E5F6"/>
<Setter Property="BorderBrush" TargetName="border" Value="#FF2C628B"/>
</Trigger>
<Trigger Property="ToggleButton.IsChecked" Value="True">
<Setter Property="Background" TargetName="border" Value="#FFBCDDEE"/>
<Setter Property="BorderBrush" TargetName="border" Value="#FF245A83"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" TargetName="border" Value="#00000000"/>
<Setter Property="BorderBrush" TargetName="border" Value="#FFADB2B5"/>
<Setter Property="Foreground" Value="#808080"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="{x:Type DataGrid}"
/>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="{x:Type DataGridColumnHeader}"
>
<Setter Property="Padding" Value="10,5" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Background" Value="#343434"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderThickness" Value="0, 0, 1, 0"/>
<Setter Property="BorderBrush" Value="Gray" />
</Style>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="{x:Type ListBox}"
>
<Setter Property="Background" Value="#181818"/>
</Style>
</Window.Resources> </Window.Resources>
<Grid Margin="10, 10"> <Window.DataContext>
<l:ViewModel/>
</Window.DataContext>
<Grid Margin="10,20,10,10">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition/> <RowDefinition/>
<RowDefinition Height="130" /> <RowDefinition Height="130" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<VirtualizingStackPanel Grid.Row="0"> <DockPanel Grid.Row="0">
<Grid Height="25"> <Grid
DockPanel.Dock="Top"
Height="25"
>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition /> <ColumnDefinition />
<ColumnDefinition Width="92"/> <ColumnDefinition Width="92"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Grid> <l:InputBar
<!--<l:InputBox HintText="输入文本,或 big/manifest/xml 文件路径"
HintText="Select some big file" Text="{Binding MainInput.Text}"
Collection="{Binding Items}" Collection="{Binding MainInput.Items}"
TextChanged="OnTextChanged" SelectedItem="{Binding MainInput.SelectedItem}"
SelectedIndex="{Binding MainInput.SelectedIndex}"
Grid.Column="0"
Margin="0,0,10,0"
/>
<Button
Content="浏览文件"
Command="{Binding MainInput.BrowseCommand}"
CommandParameter="{Binding ElementName=Self}"
Visibility="{Binding MainInput.SelectedItem, Converter={StaticResource InvalidInputEntryToVisibilityConverter}}"
Grid.Column="1"
Margin="0"
/>
<Button
Content="确认"
Command="{Binding MainInput.SelectCommand}"
CommandParameter="{Binding}"
Visibility="{Binding MainInput.SelectedItem, Converter={StaticResource ValidInputEntryToVisibilityConverter}}"
Grid.Column="1"
Margin="0"
Foreground="#20FF30"
BorderBrush="#20FF30"
/>
</Grid>
<Grid
DockPanel.Dock="Top"
Height="25"
Margin="0,10,0,0"
Visibility="Visible"
> >
</l:InputBox>--> <Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="92"/>
</Grid.ColumnDefinitions>
<l:InputBar <l:InputBar
HintText="Select some big file" HintText="输入 big 文件里包含的 manifest 文件的路径"
Text="{Binding Text}"
Collection="{Binding Items}"
TextChanged="OnMainInputTextChanged"
/>
</Grid>
<Button x:Name="button" Content="浏览文件"
Grid.Column="1" Margin="0"
/>
</Grid>
<l:InputBar
HintText="输入big文件里包含的manifest文件的路径"
Collection="{Binding Items}" Collection="{Binding Items}"
SelectedItem="{Binding SelectedItem}" SelectedItem="{Binding SelectedItem}"
Margin="0,10,0,0" Grid.Column="0"
Height="25" VerticalAlignment="Top" Margin="0,0,10,0"
/> />
<Button
Content="确认"
Grid.Column="1"
Margin="0"
Foreground="ForestGreen"
BorderBrush="ForestGreen" Template="{DynamicResource ButtonBaseControlTemplate1}"
/>
</Grid>
<DockPanel
DockPanel.Dock="Top"
Height="25"
Margin="0,10"
>
<Button
DockPanel.Dock="Right"
Content="取消加载"
Grid.Column="1"
Width="92"
/>
<Button
DockPanel.Dock="Right"
Content="加载 csf / mod.str"
Grid.Column="1"
Width="140"
Margin="10,0"
/>
<TextBlock
Text="正在加载……"
VerticalAlignment="Center"
Grid.Column="0"
/>
</DockPanel>
<l:InputBar <l:InputBar
HintText="过滤Asset ID可选" HintText="过滤Asset ID可选"
Collection="{Binding Items}" Collection="{Binding Items}"
SelectedItem="{Binding SelectedItem}" SelectedItem="{Binding SelectedItem}"
Margin="0,10,0,0" DockPanel.Dock="Top"
Height="25" VerticalAlignment="Top" Height="25"
/> />
<DataGrid <DataGrid
x:Name="DataGrid" x:Name="DataGrid"
AutoGenerateColumns="False" AutoGenerateColumns="False"
ScrollViewer.CanContentScroll="True" ScrollViewer.CanContentScroll="True"
VerticalScrollBarVisibility="Auto"
EnableRowVirtualization="True" EnableRowVirtualization="True"
VirtualizingPanel.VirtualizationMode="Recycling" VirtualizingPanel.VirtualizationMode="Recycling"
DockPanel.Dock="Top"
> >
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="Asset ID" Width="200" Binding="{Binding AssetId}"/> <DataGridTextColumn Header="Asset ID" Width="200" Binding="{Binding AssetId}"/>
<DataGridTextColumn Header="哈希" Width="*" Binding="{Binding Hash}"/> <DataGridTextColumn Header="哈希" Width="*" Binding="{Binding Hash}"/>
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</VirtualizingStackPanel> </DockPanel>
<TextBlock x:Name="textBlock" Grid.Row="1" TextWrapping="Wrap"> <TextBlock
x:Name="textBlock"
Grid.Row="1"
Margin="0,10,0,0"
TextWrapping="Wrap"
>
本工具基于 <l:ShellLink NavigateUri="https://github.com/Qibbi">Qibbi</l:ShellLink> 提供的 TechnologyAssembler 制作<LineBreak /> 本工具基于 <l:ShellLink NavigateUri="https://github.com/Qibbi">Qibbi</l:ShellLink> 提供的 TechnologyAssembler 制作<LineBreak />
此外使用了 <l:ShellLink NavigateUri="https://github.com/bgrainger">Bradley Grainger</l:ShellLink> 的 此外使用了 <l:ShellLink NavigateUri="https://github.com/bgrainger">Bradley Grainger</l:ShellLink> 的
<l:ShellLink NavigateUri="https://github.com/bgrainger/IndexRange">IndexRange</l:ShellLink> <l:ShellLink NavigateUri="https://github.com/bgrainger/IndexRange">IndexRange</l:ShellLink>

View File

@ -20,59 +20,28 @@ namespace HashCalculator.GUI
/// </summary> /// </summary>
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")] internal ViewModel ViewModel => (ViewModel)DataContext;
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
var x = new ModXml(@"D:\Users\lanyi\Desktop\RA3Mods\ARSDK2\Mods\Armor Rush\DATA\mod.xml");
var xxx = Enumerable.Repeat(new { AssetId = "正在准备", Hash = "正在准备" }, 1);
x.ProcessDocument().ContinueWith(async txx =>
{
try
{
var xx = await txx.ConfigureAwait(true);
if (x.Errors.Any())
{
MessageBox.Show("Errors: \r\n" + x.Errors.Aggregate((x, y) => $"{x}\r\n{y}"));
}
Dispatcher.Invoke(() =>
{
DataGrid.ItemsSource = from y in xx
select new { AssetId = $"{y.Type}:{y.Name}", y.Hash };
});
}
catch (Exception e)
{
Dispatcher.Invoke(() =>
{
DataGrid.ItemsSource = Enumerable.Repeat(new { AssetId = $"错误 {e}", Hash = e.ToString() }, 1);
});
}
}, TaskScheduler.Default);
/*_ = Task.Run(async () =>
{
var xxx = Enumerable.Repeat(new { AssetId = "正在准备", Hash = "正在准备" }, 1);
await Dispatcher.Invoke(async () =>
{
try
{
var xx = await x.ProcessDocument().ConfigureAwait(true);
DataGrid.ItemsSource = from y in xx
select new { AssetId = $"{y.Type}:{y.Name}", y.Hash };
}
catch (Exception e)
{
DataGrid.ItemsSource = Enumerable.Repeat(new { AssetId = $"错误 {e}", Hash = e.ToString() }, 1);
}
}).ConfigureAwait(true);
});*/
} }
private void OnMainInputTextChanged(object sender, TextChangedEventArgs e) private void OnMainInputTextChanged(object sender, TextChangedEventArgs e)
{ {
CopyableBox.ShowDialog(async token =>
{
var init = DateTimeOffset.UtcNow;
var i = 0;
while (i < 100)
{
token.ThrowIfCancellationRequested();
await Task.Delay(100).ConfigureAwait(false);
++i;
}
MessageBox.Show("Completed!");
return $"Completed after exactly {DateTimeOffset.UtcNow - init}";
});
/*var mainInput = _viewModel.MainInput; /*var mainInput = _viewModel.MainInput;
if (sender is InputBar) if (sender is InputBar)
{ {

19
ModManifest.cs Normal file
View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TechnologyAssembler.Core.Assets;
namespace HashCalculator.GUI
{
public class ModManifest
{
IReadOnlyCollection<AssetEntry> Entries;
public ModManifest()
{
var x = Manifest.Load("");
}
}
}

View File

@ -1,33 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace HashCalculator.GUI
{
internal class Model
{
private readonly ViewModel _viewModel;
private readonly FileSystemSuggestions _suggestions = new FileSystemSuggestions();
public Model(ViewModel viewModel)
{
_viewModel = viewModel;
}
public void UpdateMainInputSuggestions()
{
var mainInput = _viewModel.MainInput;
var inputValue = mainInput.Text;
if (inputValue != null)
{
mainInput.Items = _suggestions.ProvideFileSystemSuggestions(inputValue)
.Prepend(new InputEntry(InputEntryType.Text, inputValue, inputValue));
}
}
}
}

View File

@ -1,10 +1,16 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows;
using Microsoft.Win32;
using TechnologyAssembler.Core.IO;
namespace HashCalculator.GUI namespace HashCalculator.GUI
{ {
@ -37,47 +43,305 @@ namespace HashCalculator.GUI
#endregion #endregion
} }
[SuppressMessage("Microsoft.Performance", "CA1812")]
internal class ViewModel : NotifyPropertyChanged internal class ViewModel : NotifyPropertyChanged
{ {
public InputBarViewModel MainInput { get; } = new InputBarViewModel(); public MainInputViewModel MainInput { get; } = new MainInputViewModel();
public InputBarViewModel BigEntryInput { get; } = new InputBarViewModel(); public BigInputViewModel BigEntryInput { get; } = new BigInputViewModel();
public InputBarViewModel AssetIdInput { get; } = new InputBarViewModel(); public InputBarViewModel AssetIdInput { get; } = new InputBarViewModel();
public Model Model { get; }
private Visibility _bigInputVisibility = Visibility.Collapsed;
public Visibility BigInputVisibility
{
get => _bigInputVisibility;
set => SetField(ref _bigInputVisibility, value);
}
private InputEntry? _mainInputResult; // if not null
private string? _bigManifest;
public ViewModel() public ViewModel()
{ {
Model = new Model(this); }
[SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")]
public async Task OnMainInputDecided(InputEntry selected)
{
BigInputVisibility = Visibility.Collapsed;
switch (selected.Type)
{
case InputEntryType.BinaryFile:
CopyableBox.ShowDialog(async cancel =>
{
try
{
var hash = await Task.Run(() =>
{
return SageHash.CalculateBinaryHash(File.ReadAllBytes(selected.Value));
}).ConfigureAwait(false);
return $"使用Sage Hash计算出的哈希值{hash:X8} (十进制 {hash}\r\n"
+ "注意这是以大小写敏感模式计算出的哈希值与素材ID大小写不敏感的哈希其实并不完全一样";
}
catch (Exception exception)
{
return exception.ToString();
}
});
break;
case InputEntryType.BigFile:
BigInputVisibility = Visibility.Visible;
throw new NotImplementedException();
case InputEntryType.ManifestFile:
await ProcessManifest(selected).ConfigureAwait(true);
break;
case InputEntryType.XmlFile:
throw new NotImplementedException();
default:
throw new NotSupportedException();
}
}
public async Task ProcessManifest(InputEntry entry)
{
var content = await Task.Run(() =>
{
using var stream = File.OpenRead(entry.Value);
return new ManifestContent(entry.Text, stream);
}).ConfigureAwait(true);
await ProcessManifest(content).ConfigureAwait(true);
}
public async Task ProcessManifest(ManifestContent content)
{
throw new NotImplementedException();
} }
} }
internal class InputBarViewModel : NotifyPropertyChanged internal class InputBarViewModel : NotifyPropertyChanged
{ {
private IEnumerable<InputEntry>? _items; private IEnumerable<InputEntry>? _items;
public IEnumerable<InputEntry>? Items public virtual IEnumerable<InputEntry>? Items
{ {
get => _items; get => _items;
set => SetField(ref _items, value); set => SetField(ref _items, value);
} }
private InputEntry? selectedItem; private InputEntry? _selectedItem;
public InputEntry? SelectedItem public virtual InputEntry? SelectedItem
{ {
get { return selectedItem; } get => _selectedItem;
set { SetField(ref selectedItem, value); } set => SetField(ref _selectedItem, value);
}
private string? selectedValue;
public string? SelectedValue
{
get { return selectedValue; }
set { SetField(ref selectedValue, value); }
} }
private string? _text; private string? _text;
public string? Text public virtual string? Text
{ {
get { return _text; } get => _text;
set { SetField(ref _text, value); } set => SetField(ref _text, value);
}
}
internal class MainInputViewModel : InputBarViewModel
{
private readonly FileSystemSuggestions _suggestions = new FileSystemSuggestions();
private int _selectedIndex;
public int SelectedIndex
{
get => _selectedIndex;
set
{
if (value == 0)
{
if (Items.Count() <= 1)
{
value = -1;
}
else
{
value = _selectedIndex < value ? 1 : -1;
}
}
SetField(ref _selectedIndex, value);
}
}
public override string? Text
{
get => base.Text;
set
{
base.Text = value;
if (value != SelectedItem?.ToString())
{
UpdateList(value);
}
}
}
public Command<MainWindow> BrowseCommand { get; }
public Command<ViewModel> SelectCommand { get; }
public MainInputViewModel()
{
Items = Enumerable.Empty<InputEntry>();
BrowseCommand = new Command<MainWindow>(window =>
{
var dialog = new OpenFileDialog
{
Multiselect = false,
ValidateNames = true
};
if (dialog.ShowDialog(window) == true)
{
Text = dialog.FileName;
}
return Task.CompletedTask;
});
SelectCommand = new Command<ViewModel>(async viewModel =>
{
if(SelectedItem?.IsValid != true)
{
return;
}
try
{
BrowseCommand.CanExecuteValue = false;
SelectCommand.CanExecuteValue = false;
await viewModel.OnMainInputDecided(SelectedItem).ConfigureAwait(true);
}
finally
{
BrowseCommand.CanExecuteValue = true;
SelectCommand.CanExecuteValue = true;
SelectedIndex = -1;
}
});
}
private void UpdateList(string? path)
{
bool candidateFound = false;
var result = _suggestions.ProvideFileSystemSuggestions(path);
InputEntry? first = result.FirstOrDefault();
if (first != null)
{
if (new FileInfo(path!).FullName == new FileInfo(first.Value).FullName)
{
candidateFound = true;
}
}
if (!string.IsNullOrEmpty(path))
{
result = result.Prepend(new InputEntry(InputEntryType.Text, path!, path!));
}
Items = result;
if (candidateFound)
{
SelectedIndex = 1;
}
}
}
[SuppressMessage("Design", "CA1001:具有可释放字段的类型应该是可释放的", Justification = "<挂起>")]
internal class BigInputViewModel : NotifyPropertyChanged
{
private CancellationTokenSource? _currentCancellator = null;
private IEnumerable<ManifestContent>? _manifests;
public IEnumerable<ManifestContent>? Manifests
{
get => _manifests;
set => SetField(ref _manifests, value);
}
private ManifestContent? _selectedManifest;
public ManifestContent? SelectedManifest
{
get => _selectedManifest;
set
{
SetField(ref _selectedManifest, value);
SelectCommand.CanExecuteValue = value != null;
}
}
public Command<ViewModel> SelectCommand { get; }
public BigInputViewModel()
{
SelectCommand = new Command<ViewModel>(viewModel => viewModel.ProcessManifest(SelectedManifest!));
}
public async Task LoadBig(string path)
{
using var cancellator = _currentCancellator = new CancellationTokenSource();
var saved = Interlocked.Exchange(ref _currentCancellator, cancellator);
saved?.Cancel();
var token = cancellator.Token;
try
{
using var big = await Task.Run(() => new BigFile(path), token).ConfigureAwait(true);
var manifests = await Task.Run(() =>
{
var manifests = big.GetFiles(string.Empty, "*.manifest", VirtualSearchOptionType.AllDirectories);
var modManifests = from manifest in manifests
where FileNameStartsWith(manifest, "mod")
select manifest;
var globalDataManifests = from manifest in manifests
where FileNameStartsWith(manifest, "mapmetadata")
select manifest;
var firstManifests = modManifests.Concat(globalDataManifests);
var otherManifests = from manifest in manifests
where !firstManifests.Contains(manifest)
select manifest;
var list = new List<ManifestContent>();
foreach (var path in firstManifests.Concat(otherManifests))
{
token.ThrowIfCancellationRequested();
using var stream = big.OpenStream(path);
list.Add(new ManifestContent(path, stream));
}
return list;
}, token).ConfigureAwait(true);
if (!token.IsCancellationRequested)
{
Manifests = manifests;
}
}
catch (OperationCanceledException) { }
Interlocked.CompareExchange(ref _currentCancellator, null, cancellator);
}
private static bool FileNameStartsWith(string path, string what)
{
return Path.GetFileName(path).StartsWith(what, StringComparison.OrdinalIgnoreCase);
}
}
internal class ManifestContent
{
public string Name { get; }
public byte[] Data { get; }
public ManifestContent(string name, Stream stream)
{
Name = name;
using var memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);
Data = memoryStream.ToArray();
}
public override string ToString()
{
return Name;
} }
} }
} }