Group rename WIP

This commit is contained in:
2023-07-05 02:17:58 +02:00
parent 453834646b
commit f9c98ff2dc
15 changed files with 389 additions and 68 deletions

View File

@@ -0,0 +1,11 @@
using System.Collections.ObjectModel;
using FileTime.Core.Interactions;
namespace FileTime.App.Core.Interactions;
public class DoubleTextListPreview : IPreviewElement
{
public ObservableCollection<DoubleTextPreview> Items { get; } = new();
public PreviewType PreviewType { get; } = PreviewType.DoubleTextList;
object IPreviewElement.PreviewType => PreviewType;
}

View File

@@ -0,0 +1,14 @@
using System.Reactive.Subjects;
using FileTime.App.Core.Models;
using FileTime.Core.Interactions;
namespace FileTime.App.Core.Interactions;
public class DoubleTextPreview : IPreviewElement
{
public IObservable<List<ItemNamePart>> Text1 { get; init; } = new BehaviorSubject<List<ItemNamePart>>(new());
public IObservable<List<ItemNamePart>> Text2 { get; init; } = new BehaviorSubject<List<ItemNamePart>>(new());
public PreviewType PreviewType { get; } = PreviewType.DoubleTextList;
object IPreviewElement.PreviewType => PreviewType;
}

View File

@@ -0,0 +1,7 @@
namespace FileTime.App.Core.Interactions;
public enum PreviewType
{
DoubleText,
DoubleTextList
}

View File

@@ -1,4 +1,9 @@
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Text.RegularExpressions;
using DynamicData;
using FileTime.App.Core.Interactions;
using FileTime.App.Core.Models;
using FileTime.App.Core.Models.Enums; using FileTime.App.Core.Models.Enums;
using FileTime.App.Core.UserCommand; using FileTime.App.Core.UserCommand;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
@@ -175,25 +180,143 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi
private async Task Rename(RenameCommand command) private async Task Rename(RenameCommand command)
{ {
//TODO: group rename if ((_markedItems?.Collection?.Count ?? 0) > 0)
List<ItemToMove> itemsToMove = new(); {
if (_currentSelectedItem?.BaseItem?.FullName is null) return; BehaviorSubject<string> templateRegexValue = new(string.Empty);
BehaviorSubject<string> newNameSchemaValue = new(string.Empty);
var item = await _timelessContentProvider.GetItemByFullNameAsync(_currentSelectedItem.BaseItem.FullName, PointInTime.Present); var itemPreviews = _markedItems!.Collection!
.Select(item =>
{
var originalName = item.GetName();
if (item is null) return; var decoratedOriginalName = templateRegexValue.Select(templateRegex =>
{
try
{
if (string.IsNullOrWhiteSpace(templateRegex))
return new List<ItemNamePart> {new(originalName)};
var renameInput = new TextInputElement("New name", item.Name); var regex = new Regex(templateRegex);
var match = regex.Match(originalName);
if (!match.Success) return new List<ItemNamePart> {new(originalName)};
await _userCommunicationService.ReadInputs(renameInput); var matchGroups = match.Groups;
//TODO: should check these... var indices = Enumerable.Empty<int>()
var newPath = item.FullName!.GetParent()!.GetChild(renameInput.Value!); .Prepend(0)
itemsToMove.Add(new ItemToMove(item.FullName, newPath)); .Concat(
((IList<Group>) match.Groups).Skip(1).SelectMany(g => new[] {g.Index, g.Index + g.Length})
)
.Append(originalName.Length)
.ToList();
var moveCommandFactory = _serviceProvider.GetRequiredService<MoveCommandFactory>(); var itemNameParts = new List<ItemNamePart>();
var moveCommand = moveCommandFactory.GenerateCommand(itemsToMove); for (var i = 0; i < indices.Count - 1; i++)
await AddCommand(moveCommand); {
var text = originalName.Substring(indices[i], indices[i + 1] - indices[i]);
itemNameParts.Add(new ItemNamePart(text, i % 2 == 1));
}
return itemNameParts;
}
catch
{
return new List<ItemNamePart> {new(originalName)};
}
}
);
var text2 = Observable.CombineLatest(
templateRegexValue,
newNameSchemaValue,
(templateRegex, newNameSchema) =>
{
try
{
if (string.IsNullOrWhiteSpace(templateRegex)
|| string.IsNullOrWhiteSpace(newNameSchema)) return new List<ItemNamePart> {new(originalName)};
var regex = new Regex(templateRegex);
var match = regex.Match(originalName);
if (!match.Success) return new List<ItemNamePart> {new(originalName)};
var matchGroups = match.Groups;
var newNameParts = Enumerable.Range(1, matchGroups.Count).Aggregate(
(IEnumerable<string>) new List<string> {newNameSchema},
(acc, i) =>
acc.SelectMany(item2 =>
item2
.Split($"/{i}/")
.SelectMany(e => new[] {e, $"/{i}/"})
.SkipLast(1)
)
);
var itemNameParts = newNameParts.Select(namePart =>
namePart.StartsWith("/")
&& namePart.EndsWith("/")
&& namePart.Length > 2
&& int.TryParse(namePart.AsSpan(1, namePart.Length - 2), out var index)
&& index > 0
&& index <= matchGroups.Count
? new ItemNamePart(matchGroups[index].Value, true)
: new ItemNamePart(namePart, false)
);
return itemNameParts.ToList();
}
catch
{
return new List<ItemNamePart> {new(originalName)};
}
}
);
var preview = new DoubleTextPreview
{
Text1 = decoratedOriginalName,
Text2 = text2
};
return preview;
}
);
DoubleTextListPreview doubleTextListPreview = new();
doubleTextListPreview.Items.AddRange(itemPreviews);
var templateRegex = new TextInputElement("Template regex", string.Empty,
s => templateRegexValue.OnNext(s!));
var newNameSchema = new TextInputElement("New name schema", string.Empty,
s => newNameSchemaValue.OnNext(s!));
await _userCommunicationService.ReadInputs(
new[] {templateRegex, newNameSchema},
new[] {doubleTextListPreview}
);
}
else
{
List<ItemToMove> itemsToMove = new();
if (_currentSelectedItem?.BaseItem?.FullName is null) return;
var item = await _timelessContentProvider.GetItemByFullNameAsync(_currentSelectedItem.BaseItem.FullName, PointInTime.Present);
if (item is null) return;
var renameInput = new TextInputElement("New name", item.Name);
await _userCommunicationService.ReadInputs(renameInput);
//TODO: should check these...
var newPath = item.FullName!.GetParent()!.GetChild(renameInput.Value!);
itemsToMove.Add(new ItemToMove(item.FullName, newPath));
var moveCommandFactory = _serviceProvider.GetRequiredService<MoveCommandFactory>();
var moveCommand = moveCommandFactory.GenerateCommand(itemsToMove);
await AddCommand(moveCommand);
}
} }
private async Task Delete(DeleteCommand command) private async Task Delete(DeleteCommand command)

View File

@@ -4,4 +4,7 @@ public interface IInputElement
{ {
InputType Type { get; } InputType Type { get; }
string Label { get; } string Label { get; }
IObservable<bool> IsValid { get; }
void SetValid();
void SetInvalid();
} }

View File

@@ -0,0 +1,6 @@
namespace FileTime.Core.Interactions;
public interface IPreviewElement
{
object PreviewType { get; }
}

View File

@@ -3,6 +3,8 @@ namespace FileTime.Core.Interactions;
public interface IUserCommunicationService public interface IUserCommunicationService
{ {
Task<bool> ReadInputs(params IInputElement[] fields); Task<bool> ReadInputs(params IInputElement[] fields);
Task<bool> ReadInputs(IInputElement field, IEnumerable<IPreviewElement>? previews = null);
Task<bool> ReadInputs(IEnumerable<IInputElement> fields, IEnumerable<IPreviewElement>? previews = null);
void ShowToastMessage(string text); void ShowToastMessage(string text);
Task<MessageBoxResult> ShowMessageBox(string text); Task<MessageBoxResult> ShowMessageBox(string text);
} }

View File

@@ -1,13 +1,23 @@
using System.Reactive.Linq;
using System.Reactive.Subjects;
namespace FileTime.Core.Interactions; namespace FileTime.Core.Interactions;
public abstract class InputElementBase : IInputElement public abstract class InputElementBase : IInputElement
{ {
private readonly BehaviorSubject<bool> _isValid = new(true);
public InputType Type { get; } public InputType Type { get; }
public string Label { get; } public string Label { get; }
public IObservable<bool> IsValid { get; }
protected InputElementBase(string label, InputType type) protected InputElementBase(string label, InputType type)
{ {
Label = label; Label = label;
Type = type; Type = type;
IsValid = _isValid.AsObservable();
} }
public void SetValid() => _isValid.OnNext(true);
public void SetInvalid() => _isValid.OnNext(false);
} }

View File

@@ -3,12 +3,19 @@ using PropertyChanged.SourceGenerator;
namespace FileTime.Core.Interactions; namespace FileTime.Core.Interactions;
public partial class TextInputElement : InputElementBase, INotifyPropertyChanged public partial class TextInputElement : InputElementBase
{ {
[Notify] private string? _value; [Notify] private string? _value;
private readonly Action<string?>? _update;
public TextInputElement(string label, string? value = null) : base(label, InputType.Text) public TextInputElement(
string label,
string? value = null,
Action<string?>? update = null) : base(label, InputType.Text)
{ {
_value = value; _value = value;
_update = update;
} }
private void OnValueChanged(string? oldValue, string? newValue) => _update?.Invoke(newValue);
} }

View File

@@ -7,5 +7,4 @@ public interface IDialogService : IUserCommunicationService
{ {
IObservable<ReadInputsViewModel?> ReadInput { get; } IObservable<ReadInputsViewModel?> ReadInput { get; }
IObservable<MessageBoxViewModel?> LastMessageBox { get; } IObservable<MessageBoxViewModel?> LastMessageBox { get; }
void ReadInputs(IEnumerable<IInputElement> inputs, Action inputHandler, Action? cancelHandler = null);
} }

View File

@@ -1,28 +1,33 @@
using System.Collections.ObjectModel;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
using FileTime.Core.Interactions; using FileTime.Core.Interactions;
using MvvmGen; using PropertyChanged.SourceGenerator;
namespace FileTime.GuiApp.ViewModels; namespace FileTime.GuiApp.ViewModels;
[ViewModel] public class ReadInputsViewModel : IModalViewModel
[Inject(typeof(Action<ReadInputsViewModel>), "_cancel")]
[Inject(typeof(Action<ReadInputsViewModel>), "_process")]
public partial class ReadInputsViewModel : IModalViewModel
{ {
public string Name => "ReadInputs"; public string Name => "ReadInputs";
public List<IInputElement> Inputs { get; set; } public required List<IInputElement> Inputs { get; init; }
public Action SuccessHandler { get; set; } public required Action<ReadInputsViewModel> SuccessHandler { get; init; }
public Action? CancelHandler { get; set; } public required Action<ReadInputsViewModel>? CancelHandler { get; init; }
public ObservableCollection<IPreviewElement> Previews { get; } = new();
[Command] public ReadInputsViewModel()
public void Process()
{ {
_process.Invoke(this);
} }
[Command] public ReadInputsViewModel(
public void Cancel() List<IInputElement> inputs,
Action<ReadInputsViewModel> successHandler,
Action<ReadInputsViewModel>? cancelHandler = null)
{ {
_cancel.Invoke(this); Inputs = inputs;
SuccessHandler = successHandler;
CancelHandler = cancelHandler;
} }
public void Process() => SuccessHandler.Invoke(this);
public void Cancel() => CancelHandler?.Invoke(this);
} }

View File

@@ -0,0 +1,16 @@
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
namespace FileTime.GuiApp.Converters;
public class TextDecorationConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is bool b && b) return TextDecorations.Underline;
return null;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotImplementedException();
}

View File

@@ -1,10 +1,11 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" <ResourceDictionary
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns="https://github.com/avaloniaui"
xmlns:converters="using:FileTime.GuiApp.Converters"> xmlns:converters="using:FileTime.GuiApp.Converters"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://FileTime.GuiApp/Resources/Brushes.axaml"></ResourceInclude> <ResourceInclude Source="avares://FileTime.GuiApp/Resources/Brushes.axaml" />
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
@@ -26,18 +27,20 @@
x:Key="ItemViewModeToBackgroundConverter" /> x:Key="ItemViewModeToBackgroundConverter" />
<converters:NamePartShrinkerConverter x:Key="NamePartShrinkerConverter" /> <converters:NamePartShrinkerConverter x:Key="NamePartShrinkerConverter" />
<converters:ItemViewModelIsAttributeTypeConverter x:Key="ItemViewModelIsAttributeTypeConverter" /> <converters:ItemViewModelIsAttributeTypeConverter x:Key="ItemViewModelIsAttributeTypeConverter" />
<converters:ItemViewModelIsAttributeTypeConverter Invert="true" <converters:ItemViewModelIsAttributeTypeConverter Invert="true" x:Key="ItemViewModelIsNotAttributeTypeConverter" />
x:Key="ItemViewModelIsNotAttributeTypeConverter" />
<converters:GetFileExtensionConverter x:Key="GetFileExtensionConverter" /> <converters:GetFileExtensionConverter x:Key="GetFileExtensionConverter" />
<converters:FormatSizeConverter x:Key="FormatSizeConverter" /> <converters:FormatSizeConverter x:Key="FormatSizeConverter" />
<converters:DateTimeConverter x:Key="DateTimeConverter" /> <converters:DateTimeConverter x:Key="DateTimeConverter" />
<converters:SplitStringConverter x:Key="SplitStringConverter" /> <converters:SplitStringConverter x:Key="SplitStringConverter" />
<converters:CompareConverter x:Key="EqualityConverter" /> <converters:CompareConverter x:Key="EqualityConverter" />
<converters:CompareConverter ComparisonCondition="{x:Static converters:ComparisonCondition.NotEqual}" <converters:CompareConverter ComparisonCondition="{x:Static converters:ComparisonCondition.NotEqual}" x:Key="NotEqualsConverter" />
x:Key="NotEqualsConverter" />
<converters:ExceptionToStringConverter x:Key="ExceptionToStringConverter" /> <converters:ExceptionToStringConverter x:Key="ExceptionToStringConverter" />
<converters:CommandToCommandNameConverter x:Key="CommandToCommandNameConverter" /> <converters:CommandToCommandNameConverter x:Key="CommandToCommandNameConverter" />
<converters:ItemToImageConverter x:Key="ItemToImageConverter" /> <converters:ItemToImageConverter x:Key="ItemToImageConverter" />
<converters:StringReplaceConverter x:Key="PathPreformatter" OldValue="://" NewValue="/"/> <converters:StringReplaceConverter
<converters:ContextMenuGenerator x:Key="ContextMenuGenerator"/> NewValue="/"
OldValue="://"
x:Key="PathPreformatter" />
<converters:ContextMenuGenerator x:Key="ContextMenuGenerator" />
<converters:TextDecorationConverter x:Key="TextDecorationConverter" />
</ResourceDictionary> </ResourceDictionary>

View File

@@ -37,16 +37,76 @@ public class DialogService : IDialogService
.Select(m => m.LastOrDefault()); .Select(m => m.LastOrDefault());
} }
public void ReadInputs(IEnumerable<IInputElement> inputs, Action inputHandler, Action? cancelHandler = null) private void ReadInputs(
IEnumerable<IInputElement> inputs,
Action inputHandler,
Action? cancelHandler = null,
IEnumerable<IPreviewElement>? previews = null)
{ {
var modalViewModel = new ReadInputsViewModel(HandleReadInputsSuccess, HandleReadInputsCancel) var modalViewModel = new ReadInputsViewModel
{ {
Inputs = inputs.ToList(), Inputs = inputs.ToList(),
SuccessHandler = inputHandler, SuccessHandler = HandleReadInputsSuccess,
CancelHandler = cancelHandler CancelHandler = HandleReadInputsCancel
}; };
if (previews is not null)
{
modalViewModel.Previews.AddRange(previews);
}
_modalService.OpenModal(modalViewModel); _modalService.OpenModal(modalViewModel);
void HandleReadInputsSuccess(ReadInputsViewModel readInputsViewModel)
{
_modalService.CloseModal(readInputsViewModel);
inputHandler();
}
void HandleReadInputsCancel(ReadInputsViewModel readInputsViewModel)
{
_modalService.CloseModal(readInputsViewModel);
cancelHandler?.Invoke();
}
}
public Task<bool> ReadInputs(IEnumerable<IInputElement> fields, IEnumerable<IPreviewElement>? previews = null)
{
var taskCompletionSource = new TaskCompletionSource<bool>();
ReadInputs(
fields,
() => taskCompletionSource.SetResult(true),
() => taskCompletionSource.SetResult(false),
previews
);
return taskCompletionSource.Task;
}
public Task<bool> ReadInputs(params IInputElement[] fields)
{
var taskCompletionSource = new TaskCompletionSource<bool>();
ReadInputs(
fields,
() => taskCompletionSource.SetResult(true),
() => taskCompletionSource.SetResult(false)
);
return taskCompletionSource.Task;
}
public Task<bool> ReadInputs(IInputElement field, IEnumerable<IPreviewElement>? previews = null)
{
var taskCompletionSource = new TaskCompletionSource<bool>();
ReadInputs(
new[] {field},
() => taskCompletionSource.SetResult(true),
() => taskCompletionSource.SetResult(false),
previews
);
return taskCompletionSource.Task;
} }
public void ShowToastMessage(string text) public void ShowToastMessage(string text)
@@ -70,24 +130,4 @@ public class DialogService : IDialogService
return taskCompletionSource.Task; return taskCompletionSource.Task;
} }
private void HandleReadInputsSuccess(ReadInputsViewModel readInputsViewModel)
{
_modalService.CloseModal(readInputsViewModel);
readInputsViewModel.SuccessHandler.Invoke();
}
private void HandleReadInputsCancel(ReadInputsViewModel readInputsViewModel)
{
_modalService.CloseModal(readInputsViewModel);
readInputsViewModel.CancelHandler?.Invoke();
}
public Task<bool> ReadInputs(params IInputElement[] fields)
{
var taskCompletionSource = new TaskCompletionSource<bool>();
ReadInputs(fields, () => taskCompletionSource.SetResult(true), () => taskCompletionSource.SetResult(false));
return taskCompletionSource.Task;
}
} }

View File

@@ -19,6 +19,7 @@
x:DataType="vm:IMainWindowViewModelBase" x:DataType="vm:IMainWindowViewModelBase"
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:appCoreModels="using:FileTime.App.Core.Models" xmlns:appCoreModels="using:FileTime.App.Core.Models"
xmlns:appInteractions="using:FileTime.App.Core.Interactions"
xmlns:config="using:FileTime.GuiApp.Configuration" xmlns:config="using:FileTime.GuiApp.Configuration"
xmlns:corevm="using:FileTime.App.Core.ViewModels" xmlns:corevm="using:FileTime.App.Core.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
@@ -578,7 +579,7 @@
HorizontalAlignment="Center" HorizontalAlignment="Center"
Padding="20" Padding="20"
VerticalAlignment="Center"> VerticalAlignment="Center">
<Grid RowDefinitions="Auto,Auto"> <Grid RowDefinitions="Auto,Auto,Auto">
<ItemsControl <ItemsControl
ItemsSource="{Binding DialogService.ReadInput^.Inputs}" ItemsSource="{Binding DialogService.ReadInput^.Inputs}"
@@ -621,18 +622,92 @@
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
<ItemsControl Grid.Row="1" ItemsSource="{Binding DialogService.ReadInput^.Previews}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid IsVisible="{Binding PreviewType, Converter={StaticResource EqualityConverter}, ConverterParameter={x:Static appInteractions:PreviewType.DoubleTextList}}">
<ItemsControl ItemsSource="{Binding Items}" x:DataType="appInteractions:DoubleTextListPreview">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="*,*">
<ItemsControl ItemsSource="{Binding Text1^}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" TextDecorations="{Binding IsSpecial, Converter={StaticResource TextDecorationConverter}}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl Grid.Column="1" ItemsSource="{Binding Text2^}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" TextDecorations="{Binding IsSpecial, Converter={StaticResource TextDecorationConverter}}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!--DataGrid ItemsSource="{Binding Items}" x:DataType="appInteractions:DoubleTextListPreview">
<DataGrid.Columns>
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ItemsControl
HorizontalAlignment="Stretch"
ItemsSource="{Binding Text1^}"
Margin="5,0,0,0"
VerticalAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="x:String">
<Grid>
<TextBlock Text="{Binding}" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid-->
</Grid>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel <StackPanel
DataContext="{Binding DialogService.ReadInput^}" DataContext="{Binding DialogService.ReadInput^}"
Grid.Row="1" Grid.Row="2"
Margin="0,10,0,0" Margin="0,10,0,0"
Orientation="Horizontal"> Orientation="Horizontal">
<Button <Button
Command="{Binding ProcessCommand}" Command="{Binding Process}"
Content="Ok" Content="Ok"
HorizontalContentAlignment="Center" HorizontalContentAlignment="Center"
Width="80" /> Width="80" />
<Button <Button
Command="{Binding CancelCommand}" Command="{Binding Cancel}"
Content="Cancel" Content="Cancel"
HorizontalContentAlignment="Center" HorizontalContentAlignment="Center"
Margin="10,0,0,0" Margin="10,0,0,0"