Давайте напишем класс, представляющий "плитку":
class Tile : Vm
{
int x;
public int X
{
get => x;
set => Set(ref x, value);
}
int y;
public int Y
{
get => y;
set => Set(ref y, value);
}
string caption;
public string Caption
{
get => caption;
set => Set(ref caption, value);
}
}
Вы в своем классе будете хранить картинку или ссылку на нее, я использую просто строковую надпись string Caption
Теперь главная ViewModel:
class MainVM : Vm
{
ObservableCollection<Tile> tiles;
public ObservableCollection<Tile> Tiles
{
get => tiles;
set => Set(ref tiles, value);
}
RelayCommand turnCommand;
public ICommand TurnCommand => turnCommand;
public MainVM()
{
Tiles = new ObservableCollection<Tile>
{
new Tile { Caption="1", X = 0, Y = 0 },
new Tile { Caption="2", X = 1, Y = 0 },
new Tile { Caption="3", X = 2, Y = 0 },
new Tile { Caption="4", X = 2, Y = 1 },
new Tile { Caption="5", X = 2, Y = 2 },
new Tile { Caption="6", X = 1, Y = 2 },
new Tile { Caption="7", X = 0, Y = 2 },
new Tile { Caption="8", X = 0, Y = 1 },
};
turnCommand = new RelayCommand(_ => TurnTiles());
}
void TurnTiles()
{
// Здесь используются фичи C# 7.0 и .NET Framework 4.7
Dictionary<(int, int), (int, int)> transitions = new Dictionary<(int, int), (int, int)>
{
[(0, 0)] = (0, 1),
[(1, 0)] = (0, 0),
[(2, 0)] = (1, 0),
[(2, 1)] = (2, 0),
[(2, 2)] = (2, 1),
[(1, 2)] = (2, 2),
[(0, 2)] = (1, 2),
[(0, 1)] = (0, 2),
};
foreach (var tile in Tiles)
(tile.X, tile.Y) = transitions[(tile.X, tile.Y)];
/* Вариант "по-старинке", для тех, у кого по каким-то причинам не работает вариант выше
Dictionary<Tuple<int, int>, Tuple<int, int>> transitions = new Dictionary<Tuple<int, int>, Tuple<int, int>>
{
[Tuple.Create(0, 0)] = Tuple.Create(0, 1),
[Tuple.Create(1, 0)] = Tuple.Create(0, 0),
[Tuple.Create(2, 0)] = Tuple.Create(1, 0),
[Tuple.Create(2, 1)] = Tuple.Create(2, 0),
[Tuple.Create(2, 2)] = Tuple.Create(2, 1),
[Tuple.Create(1, 2)] = Tuple.Create(2, 2),
[Tuple.Create(0, 2)] = Tuple.Create(1, 2),
[Tuple.Create(0, 1)] = Tuple.Create(0, 2),
};
foreach (var tile in Tiles)
{
var coords = transitions[Tuple.Create(tile.X, tile.Y)];
tile.X = coords.Item1;
tile.Y = coords.Item2;
}
*/
}
}
В общем-то все просто, коллекция с "плитками" и команда для их перемещения по кругу. Причем в этом примере использовать ObservableCollection даже не обязательно, вы можете использовать обычный List.
transitions - это словарь переходов, ключ словаря - начальные координаты плитки, значение - конечные
Теперь займемся представлением.
Напишем конвертеры координат VM => View:
class XToLeftConverter : IValueConverter
{
public double TileWidth { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (int)value * TileWidth;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
И
class YToTopConverter : IValueConverter
{
public double TileHeight { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (int)value * TileHeight;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
В них определено публичное свойство, которое будет устанавливаться из разметки.
Теперь сама разметка окна, в качестве примера я использовал этот ответ:
<Window x:Class="WpfTest.MainWindow"
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:WpfTest"
d:DataContext="{d:DesignInstance Type=local:MainVM}"
mc:Ignorable="d" WindowStartupLocation="CenterScreen"
Title="MainWindow" Height="400" Width="400">
<Window.Resources>
<local:XToLeftConverter x:Key="XToLeftConverter" TileWidth="100"/>
<local:YToTopConverter x:Key="YToTopConverter" TileHeight="100"/>
</Window.Resources>
<Grid Margin="5">
<ItemsControl ItemsSource="{Binding Tiles}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Height="99" Width="99"
BorderThickness="1" BorderBrush="Black">
<TextBlock Text="{Binding Caption}"
VerticalAlignment="Center" HorizontalAlignment="Center"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Left"
Value="{Binding X, Converter={StaticResource XToLeftConverter}}"/>
<Setter Property="Canvas.Top"
Value="{Binding Y, Converter={StaticResource YToTopConverter}}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
<Button Content="Rotate" Command="{Binding TurnCommand}"
Width="99" Height="99" Margin="100,100,0,0"
VerticalAlignment="Top" HorizontalAlignment="Left"/>
</Grid>
</Window>
Немного кривовато, но пока так.
В принципе это уже работает, но, пока, без анимации. При клике по кнопке в центре плитки перепрыгивают по кругу.
Теперь это все дело нужно анимировать, я использовал этот ответ. Я пока не знаю, как использовать этот класс-хелпер для анимации двух свойств, поэтому завел два класса с одинаковым содержимым (как появится информация - обновлю ответ): AnimatableLeftHelper и AnimatableTopHelper.
Теперь используем эти хелперы чтобы анимировать свойства:
<Style>
<Setter Property="local:AnimatableLeftHelper.OriginalProperty"
Value="{Binding X, Converter={StaticResource XToLeftConverter}}"/>
<Setter Property="Canvas.Left"
Value="{Binding (local:AnimatableLeftHelper.AnimatedProperty), RelativeSource={RelativeSource Self}}"/>
<Setter Property="local:AnimatableTopHelper.OriginalProperty"
Value="{Binding Y, Converter={StaticResource YToTopConverter}}"/>
<Setter Property="Canvas.Top"
Value="{Binding (local:AnimatableTopHelper.AnimatedProperty), RelativeSource={RelativeSource Self}}"/>
</Style>
Готово!

Здесь я использовал "стандартные" классы для MVVM WPF
Vm:
abstract class Vm : INotifyPropertyChanged
{
protected bool Set<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
NotifyPropertyChanged(propertyName);
return true;
}
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
RelayCommand:
class RelayCommand : ICommand
{
protected readonly Predicate<object> _canExecute;
protected readonly Action<object> _execute;
public event EventHandler CanExecuteChanged;
public RelayCommand(Action<object> execute)
: this(execute, _ => true) { }
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute ?? throw new ArgumentNullException(nameof(canExecute));
}
public bool CanExecute(object parameter) => _canExecute(parameter);
public void Execute(object parameter) => _execute(parameter);
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
Благодаря помощи @VladD'а удалось переписать пример с использованием PointAnimation:
"Плитка":
class Tile : Vm
{
Point coords;
public Point Coords
{
get => coords;
set => Set(ref coords, value);
}
string caption;
public string Caption
{
get => caption;
set => Set(ref caption, value);
}
}
Главная VM:
class MainVM : Vm
{
ObservableCollection<Tile> tiles;
public ObservableCollection<Tile> Tiles
{
get => tiles;
set => Set(ref tiles, value);
}
public ICommand TurnCommand { get; }
public MainVM()
{
Tiles = new ObservableCollection<Tile>
{
new Tile { Caption="1", Coords = new Point(0, 0) },
new Tile { Caption="2", Coords = new Point(1, 0) },
new Tile { Caption="3", Coords = new Point(2, 0) },
new Tile { Caption="4", Coords = new Point(2, 1) },
new Tile { Caption="5", Coords = new Point(2, 2) },
new Tile { Caption="6", Coords = new Point(1, 2) },
new Tile { Caption="7", Coords = new Point(0, 2) },
new Tile { Caption="8", Coords = new Point(0, 1) }
};
TurnCommand = new DelegateCommand(_ => TurnTiles());
}
void TurnTiles()
{
Dictionary<Point, Point> transitions = new Dictionary<Point, Point>
{
[new Point(0, 0)] = new Point(0, 1),
[new Point(1, 0)] = new Point(0, 0),
[new Point(2, 0)] = new Point(1, 0),
[new Point(2, 1)] = new Point(2, 0),
[new Point(2, 2)] = new Point(2, 1),
[new Point(1, 2)] = new Point(2, 2),
[new Point(0, 2)] = new Point(1, 2),
[new Point(0, 1)] = new Point(0, 2),
};
foreach (var tile in Tiles)
tile.Coords = transitions[tile.Coords];
}
}
Конвертер всего один теперь:
class XYToPointConverter : IValueConverter
{
public double TileWidth { get; set; }
public double TileHeight { get; set; }
public double LeftOffset { get; set; }
public double TopOffset { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var typedValue = (Point)value;
return new Point(typedValue.X * TileWidth + LeftOffset, typedValue.Y * TileHeight + TopOffset);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Я добавил в него еще пару свойств со смещениями, чтобы настраивать положение плиток можно было более гибко.
Разметка окна:
<Window x:Class="WpfTest.MainWindow"
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:WpfTest"
d:DataContext="{d:DesignInstance Type=local:MainVM, IsDesignTimeCreatable=True}"
mc:Ignorable="d" WindowStartupLocation="CenterScreen"
Title="MainWindow" Height="400" Width="400">
<Window.Resources>
<local:XYToPointConverter x:Key="XYToPointConverter"
TileWidth="100" TileHeight="100"
LeftOffset="0" TopOffset="0"/>
</Window.Resources>
<Grid Margin="5">
<ItemsControl ItemsSource="{Binding Tiles}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Height="99" Width="99"
BorderThickness="1" BorderBrush="Black">
<TextBlock Text="{Binding Caption}"
VerticalAlignment="Center" HorizontalAlignment="Center"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="local:AnimatablePointHelper.OriginalProperty"
Value="{Binding Coords, Converter={StaticResource XYToPointConverter}}"/>
<Setter Property="Canvas.Left"
Value="{Binding (local:AnimatablePointHelper.AnimatedProperty).X, RelativeSource={RelativeSource Self}}"/>
<Setter Property="Canvas.Top"
Value="{Binding (local:AnimatablePointHelper.AnimatedProperty).Y, RelativeSource={RelativeSource Self}}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
<Button Content="Rotate" Command="{Binding TurnCommand}"
Width="99" Height="99" Margin="100,100,0,0"
VerticalAlignment="Top" HorizontalAlignment="Left"/>
</Grid>
</Window>
Ну и класс-хелпер для анимации:
static class AnimatablePointHelper
{
public static Point GetOriginalProperty(DependencyObject obj) =>
(Point)obj.GetValue(OriginalPropertyProperty);
public static void SetOriginalProperty(DependencyObject obj, Point value) =>
obj.SetValue(OriginalPropertyProperty, value);
public static readonly DependencyProperty OriginalPropertyProperty =
DependencyProperty.RegisterAttached(
"OriginalProperty", typeof(Point), typeof(AnimatablePointHelper),
new PropertyMetadata(OnOriginalUpdatedStatic));
public static Point GetAnimatedProperty(DependencyObject obj) =>
(Point)obj.GetValue(AnimatedPropertyProperty);
public static void SetAnimatedProperty(DependencyObject obj, Point value) =>
obj.SetValue(AnimatedPropertyProperty, value);
public static readonly DependencyProperty AnimatedPropertyProperty =
DependencyProperty.RegisterAttached(
"AnimatedProperty", typeof(Point), typeof(AnimatablePointHelper));
static void OnOriginalUpdatedStatic(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
Point newValue = (Point)e.NewValue;
FrameworkElement self = (FrameworkElement)o;
AnimationTimeline animation =
new PointAnimation(newValue, new Duration(TimeSpan.FromSeconds(0.3)));
self.BeginAnimation(AnimatedPropertyProperty, animation);
}
}
Результат тот же.
(int, int)наTuple<int, int>, но там понадобится исправить и другие ошибки компиляции, которые произойдут из-за замены. – VladD Jul 30 '17 at 11:17(0, 0)->Tuple.Create(0, 0)и т. д. – VladD Jul 30 '17 at 11:34