Как отделить логику от дизайна у кнопок? Как забиндить коллекцию кнопок в View?
Подскажите,пожалуйста, у меня имеется класс кнопки:
public class Button : INotifyPropertyChanged
{
public int Left { get; }
public int Top { get; }
public Button(int left, int top)
{
Left = left;
Top = top;
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName]string prop = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
private bool isState;
public bool IsState
{
get
{
return isState;
}
set
{
isState = value;
OnPropertyChanged("IsState");
}
}
}
Имеется VM для кнопок, в котором находятся а)список с инициализацией восьми кнопок:
public ObservableCollection<Button> Buttons { get; } = new ObservableCollection<Button>
{
new Button(437, 153), // Button[0]
new Button(396, 248), // Button[1]
new Button(558, 250), // Button[2]
new Button(661, 253), // Button[3]
new Button(267, 246), // Button[4]
new Button(873, 252), // Button[5]
new Button(667, 505), // Button[6]
new Button(875, 506), // Button[7]
};
б) одна команда для изменения состояния каждой кнопки, в зависимости какую нажать:
private RelayCommand stateCommand;
public RelayCommand StateCommand
{
get
{
return stateCommand ??
(stateCommand = new RelayCommand(obj =>
{
ChangeState((Button)obj);
}));
}
}
в) сам метод ChangeState, который меняет состояние кнопки:
void ChangeState(Button button)
{
button.IsState = !button.IsState;
}
А реализация коллекции кнопок в View сделана таким образом:
<ItemsControl ItemsSource="{Binding Buttons}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding Left}"/>
<Setter Property="Canvas.Top" Value="{Binding Top}"/>
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Button Width="48" Command="{Binding DataContext.StateCommand, ElementName=mainWindow}" CommandParameter="{Binding}">
<Image Style="{StaticResource image}" Height="22"/>
</Button>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
У кнопки есть вложенная Image, к ней подсоединен стиль:
<Style x:Key="image">
<Style.Triggers>
<DataTrigger Binding="{Binding IsState}" Value="False">
<Setter Property="Image.Source" Value="Resources/image1.png" />
</DataTrigger>
<DataTrigger Binding="{Binding IsState}" Value="True">
<Setter Property="Image.Source" Value="Resources/image2.png" />
</DataTrigger>
</Style.Triggers>
</Style>
В итоге код функционирует как и от него требовалось, а именно: кликаем на любую кнопку из списка восьми кнопок, и на кликнутой кнопке меняется картинка. Всё замечательно, за исключением, как видите, я ручками вставляю значения Canvas.Top и Canvas.Left, так как при такой компоновке кнопки не отображаются во время разработки в окне макета, а появляются только при запуске программы (это так же является минусом, от которого хочется избавиться).
Вопрос: как написать код так, чтобы у меня также на все 8 кнопок осталась одна команда, которая понимает у какой конкретно кнопки надо поменять состояние, но при этом, чтоб каждую кнопку я мог выводить в View xaml коде отдельно и менять у каждой кнопки свойства, ну как обычно, что-то типа такого:
<Button x:Name="button1" Width="41" Height="38" command="тут бинд команды" Opacity="1" Canvas.Left="100" Canvas.Top="146">
<Image Style="{StaticResource image}" Height="22"/>
</Button>
<Button x:Name="button2" Width="50" Height="12" command="тут бинд команды" Opacity="1" Canvas.Left="200" Canvas.Top="346">
<Image Style="{StaticResource image}" Height="22"/>
</Button>
и т.д., тобишь, чтоб кнопки были отображены на макете?
Ответы (1 шт):
Предположим, что у нас есть ViewModel челвоека:
class Person
{
public Person(string name, int age)
=> (Name, Age) = (name, age);
public string Name { get; set; }
public int Age { get; set; }
}
В главной VM мы например делаем список людей:
class MainViewModel
{
public List<Person> Persons { get; set; } = new List<Person>
{
new Person("Вася", 13),
new Person("Петя", 6)
};
}
В XAML привязываем все и задаем нужный нам вид:
<ItemsControl ItemsSource="{Binding Persons}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name, StringFormat=Имя: {0}}"/>
<TextBlock Text="{Binding Age, StringFormat=Возраст: {0}}" Margin="5 0 0 0"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
При запуске данные есть:
А в дизайнере пустой, белый лист:
Вопрос, как отобразить данные в дизайнере? Да все довольно просто:
В View слое (XAML), задаем DataContext для дизайнера, указав ему нужный тип данных:
d:DataContext="{d:DesignInstance {x:Type vm:MainViewModel}}"
И после этого студия начнем нам выдавать подсказки по именам, а также отобразит тестовые данные в дизайнере:
Мы можем немного усовершенствовать это все, включив у DesignInstance свойство IsDesignTimeCreatable, тогда получим следующий вид строки:
d:DataContext="{d:DesignInstance {x:Type vm:MainViewModel}, IsDesignTimeCreatable=True}"
В таком случае студия не будет делать свои "фейковые" данные, а попытается создать объект для отображения с заготовленными для нее данными, в моем случае данные статичны, они прописаны при инициализации коллекции и студия их примет:
Если вам нужны данные для динамичного объекта, то стоит почитать этот ответ.



