I have a TemplatedControl SoftwareReleaseControl, which displays some texts and a button. I need this button to inherit its Click event from the property OnInstallClick that is specified when creating the SoftwareReleaseControl control.
The problem is: I can't make it work. It does not bind to the template's property. I've tried copying source code from Avalonia's button (ClickEvent) to the control's Code-Behind. It shows as an EventHandler, but is not passed to the button, and also gives an Unable to find suitable setter or adder [...] error.
SoftwareReleaseControl.xaml:
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="using:Updater.Controls">
<Design.PreviewWith>
<StackPanel Spacing="5">
<Panel Classes="Spacing"/>
<my:SoftwareReleaseControl Title="..." Version="..." Description="..." Installed="..."/>
<my:SoftwareReleaseControl Title="..." Version="..." Description="..." Installed="..."/>
</StackPanel>
</Design.PreviewWith>
<Style Selector="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource text}"/>
<Setter Property="FontFamily" Value="Lato"/>
</Style>
[...]
<Style Selector="my|SoftwareReleaseControl">
[...]
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Panel Background="{TemplateBinding Background}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Height="{TemplateBinding Height}"
MinWidth="{TemplateBinding MinWidth}" Width="{TemplateBinding Width}">
<Grid Margin="{TemplateBinding Padding}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
RowDefinitions="46, *, 40">
[...]
<StackPanel Grid.Row="2" Orientation="Horizontal">
======= HERE ===>> <Button x:Name="PART_footer_installButton"
Content="Instalar"
======= PROBLEM ===>> Click="{TemplateBinding OnInstallClick}">
<Button.Styles>
<Style Selector="Button#PART_footer_installButton">
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="Foreground" Value="{DynamicResource text}"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="Tag" Value="{TemplateBinding Tag}"/>
</Style>
<Style Selector="Button#PART_footer_installButton:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{DynamicResource text}"/>
</Style>
</Button.Styles>
</Button>
</StackPanel>
</Grid>
</Panel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Styles>
SoftwareReleaseControl.xaml.cs (code-behind):
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using System;
namespace Updater.Controls
{
public partial class SoftwareReleaseControl : TemplatedControl
{
public SoftwareReleaseControl()
{
}
public static readonly RoutedEvent<RoutedEventArgs> OnInstallClickEvent = RoutedEvent.Register<Button, RoutedEventArgs>(nameof(OnInstallClick), RoutingStrategies.Bubble);
public event EventHandler<RoutedEventArgs> OnInstallClick
{
add => AddHandler(OnInstallClickEvent, value);
remove => RemoveHandler(OnInstallClickEvent, value);
}
[...]
}
}
MainWindow.xaml (where I'm trying to show the controls):
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Updater.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:my="using:Updater.Controls"
mc:Ignorable="d" Width="800" Height="520"
x:Class="Updater.Views.MainWindow"
Icon="/Assets/avalonia-logo.ico"
Title="Updater">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Background="{DynamicResource window.background}"
RowDefinitions="40, *" ColumnDefinitions="*">
[...]
<ScrollViewer Grid.Row="1">
<StackPanel Spacing="5" x:Name="stk_releases">
<Panel Classes="Spacing"/>
==== HERE ===>> <my:SoftwareReleaseControl Title="..." Version="..." Description="..." Installed="..." OnInstallClick="{Binding btn_OnClick}"/>
<my:SoftwareReleaseControl Title="..." Version="..." Description="..." Installed="..." OnInstallClick="{Binding btn_OnClick}"/>
</StackPanel>
</ScrollViewer>
</Grid>
</Window>
It doesn't really matter if I will need to implement btn_OnClick in the MainWindow's code-behind or in the ViewModel.
The current code is giving me the errors:
Unable to find suitable setter or adder for property OnInstallClick of type Updater:Updater.Controls.SoftwareReleaseControl for argument Avalonia.Markup:Avalonia.Data.Binding, available setter parameter lists are: System.EventHandler[[Avalonia.Interactivity.RoutedEventArgs, Avalonia.Interactivity]] on MainWindow.xaml, on the OnInstallClick="{Binding btn_OnClick}".
Unable to find suitable setter or adder for property Click of type Avalonia.Controls:Avalonia.Controls.Button for argument Avalonia.Base:Avalonia.Data.IBinding, available setter parameter lists are: System.EventHandler1<Avalonia.Interactivity.RoutedEventArgs> on *SoftwareReleaseControl.xaml*, on the Click="{TemplateBinding OnInstallClick}".
Yes, I did specify the StyleInclude on App.xaml.
Why Click and not Command: I need the sender object so I can get the Tag property of the button. There will be many of this control on the window and I need to sort out which one got clicked.
tl;dr: How can I specify an event handler on my templated control, in a way that the button inside it can inherit the handler as its Click (not Command). Where will I need to implement the handler? ViewModel or CodeBehind?
I gave up on using Click, and instead found a way to send the button itself to a Command.
SoftwareReleaseControl.xaml:
[...]
<Button x:Name="PART_footer_installButton"
Command="{Binding _OnInstallClick}"
CommandParameter="{Binding RelativeSource={RelativeSource Self}}">
<Button.Styles>
<Style Selector="Button#PART_footer_installButton">
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="Foreground" Value="{DynamicResource text}"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="Tag" Value="{TemplateBinding Tag}"/>
</Style>
<Style Selector="Button#PART_footer_installButton:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{DynamicResource text}"/>
</Style>
</Button.Styles>
</Button>
[...]
SoftwareReleaseControl.xaml.cs:
namespace Updater.Controls
{
public partial class SoftwareReleaseControl : TemplatedControl
{
public SoftwareReleaseControl()
{
DataContext = this;
}
public event EventHandler InstallClick;
private void _OnInstallClick(object? sender)
{
EventHandler handler = InstallClick;
handler?.Invoke(sender, EventArgs.Empty);
}
[...]
}
}
MainWindow.xaml.cs:
namespace Updater.Views
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel(this);
for (int i = 0; i < 6; i++)
{
var t = new SoftwareReleaseControl();
t.Description = (string)App.Current.Resources["lorem.50"];
t.Installed = i % 2 == 0;
t.Tag = i;
t.InstallClick += T_InstallClick;
stk_releases.Children.Add(t);
}
}
private void T_InstallClick(object? sender, EventArgs e)
{
Debug.WriteLine("123");
if (sender is Button btn)
{
Debug.WriteLine(btn.Tag);
}
}
}
}
I've created a IconButton for use in WPF/XMAML. It should be able to display an Icon MDL2 Assets font on top and an text on bottom. It should have the appearance of an default WPF toolbar button. I decided to create a custom control which inherits from default WPF button.
So I created the custom control and added Dependency Properties for Text and the somehow cryptic MDL2IconCode:
public class IconButton : Button
{
public static readonly DependencyProperty TextProperty;
public static readonly DependencyProperty MDL2IconCodeProperty;
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public string MDL2IconCode
{
get { return (string)GetValue(MDL2IconCodeProperty); }
set { SetValue(MDL2IconCodeProperty, value); }
}
static IconButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(IconButton),
new FrameworkPropertyMetadata(typeof(IconButton)));
TextProperty = DependencyProperty.Register("Text",
typeof(string),
typeof(IconButton),
new PropertyMetadata("Button text", OnTextChanged));
MDL2IconCodeProperty = DependencyProperty.Register("MDL2IconCode",
typeof(string),
typeof(IconButton),
new PropertyMetadata("\uf13e", OnIconTextChanged));
}
static void OnTextChanged(DependencyObject o,
DependencyPropertyChangedEventArgs e)
{
var iconButton = o as IconButton;
if (iconButton == null)
{
return;
}
string newText = e.NewValue as string;
iconButton.Text = newText;
}
static void OnIconTextChanged(DependencyObject o,
DependencyPropertyChangedEventArgs e)
{
var iconButton = o as IconButton;
if (iconButton == null)
{
return;
}
string newText = e.NewValue as string;
iconButton.MDL2IconCode = newText;
}
}
The ResourceDictionary of Generic.xaml looks like this:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:UI.CustomControls">
<Style TargetType="{x:Type local:IconButton}"
BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:IconButton}">
<Button Style="{StaticResource {x:Static ToolBar.ButtonStyleKey}}">
<StackPanel>
<TextBlock HorizontalAlignment="Center"
Text="{TemplateBinding MDL2IconCode}"
FontFamily="Segoe MDL2 Assets"
FontSize="16"
x:Name="iconTextBlock"/>
<TextBlock HorizontalAlignment="Center"
Text="{TemplateBinding Text}"
x:Name="textTextBlock"/>
</StackPanel>
</Button>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
The button looks as it should.
But the command binding in XAML isn't working anymore. But it should work, as per inheritance it still is a button..
Maybe anyone has an idea what to add to make the command binding work?
Bind the command of the Button inside of your control template to the templated parent.
<ControlTemplate TargetType="{x:Type local:IconButton}">
<Button Style="{StaticResource {x:Static ToolBar.ButtonStyleKey}}"
Command="{TemplateBinding Command}"
CommandParameter="{TemplateBinding CommandParameter}"
CommandTarget="{TemplateBinding CommandTarget}">
<!-- ...other code. -->
</Button>
</ControlTemplate>
But it should work, as per inheritance it still is a button..
No. The Button inside of your control template does not magically bind to the corresponding properties of its templated parent, regardless if it is derived from Button or any other control. You will have to do so for other dependency properties like the CommandParameter as well.
Please also note that TemplateBinding is an optimized binding that does not have all capabilities of the more powerful Binding markup extension. Consequently, when TemplateBinding does not work, e.g. in two-way binding scenarios, you can use TemplatedParent like this:
{Binding RelativeSource={RelativeSource TemplatedParent}, Path=MyDependencyProperty}
For more information, you can refer to the TemplateBinding documentation.
The ControlTemplate of a Button shouldn't include another Button.
You should template your control to look like a Button if that's what you want:
<Style TargetType="{x:Type local:IconButton}" BasedOn="{StaticResource {x:Static ToolBar.ButtonStyleKey}}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:IconButton}">
<Border Name="Bd" Background="{TemplateBinding Control.Background}"
BorderBrush="{TemplateBinding Control.BorderBrush}"
BorderThickness="{TemplateBinding Control.BorderThickness}"
Padding="{TemplateBinding Control.Padding}" SnapsToDevicePixels="true">
<ContentControl HorizontalAlignment="{TemplateBinding Control.HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding Control.VerticalContentAlignment}"
SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}">
<StackPanel>
<TextBlock HorizontalAlignment="Center"
Text="{TemplateBinding MDL2IconCode}"
FontFamily="Segoe MDL2 Assets"
FontSize="16"
x:Name="iconTextBlock"/>
<TextBlock HorizontalAlignment="Center"
Text="{TemplateBinding Text}"
x:Name="textTextBlock"/>
</StackPanel>
</ContentControl>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="UIElement.IsMouseOver" Value="true">
<Setter TargetName="Bd" Value="#80DADADA" Property="BorderBrush"/>
<Setter TargetName="Bd" Value="#FFB6BDC5" Property="Background"/>
</Trigger>
<Trigger Property="UIElement.IsKeyboardFocused" Value="true">
<Setter TargetName="Bd" Value="#80DADADA" Property="BorderBrush"/>
<Setter TargetName="Bd" Value="#FFB6BDC5" Property="Background"/>
</Trigger>
<Trigger Property="ButtonBase.IsPressed" Value="true">
<Setter TargetName="Bd" Value="#90006CD9" Property="BorderBrush"/>
<Setter TargetName="Bd" Value="#400080FF" Property="Background"/>
</Trigger>
<Trigger Property="UIElement.IsEnabled" Value="false">
<Setter Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" Property="Foreground"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
It will then behave like any other Button which means that you can bind its Command property as usual:
<local:IconButton Command="{Binding YourCommand}" />
I created a context menu for a tabcontrol which changes the name of the tab. However, if the mouse over on an unselected tab and mouse right clicked on context menu pops up. If I click the the menu item it changes the selected tab's name. For instance, I right clicked on Favorite 4 tab and tried to change its name but it changed first tab's name (selected tab) as shown below.
I would like to select the tab with right mouse click as well as with left mouse click so it will not cause confusion or not intentional tab name change.
XAML
<TabControl x:Name="FavoritesTabs" HorizontalAlignment="Stretch" Height="23"
Initialized="FavoritesTabs_Initialized" Margin="8,0,7,0"
MouseRightButtonDown="FavoritesTabs_MouseRightButtonDown" MouseEnter="FavoritesTabs_MouseEnter" >
<TabControl.ContextMenu>
<ContextMenu Name="tabContextMenu">
<MenuItem Header="Change Tab Name" Click="MenuItem_Click" />
<MenuItem Header="Save Favorite Layers" />
</ContextMenu>
</TabControl.ContextMenu>
</TabControl>
C#
private void FavoritesTabs_Initialized(object sender, EventArgs e)
{
FavoritesList.Add("Favorite 1");
FavoritesList.Add("Favorite 2");
FavoritesList.Add("Favorite 3");
FavoritesList.Add("Favorite 4");
FavoritesTabs.ItemsSource = FavoritesList;
}
private void FavoritesTabs_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
}
private void MenuItem.Click(object sender, RoutedEventArgs e)
{
int index = FavoritesTabs.SelectedIndex;
FavoritesList[index] = "New tab";
}
I tried this answer but it did not work.
You have to style your TabControl adding event for MouseDown then select the tab item accordingly.
This is the code:
XAML
At fist, you need to define reosurces for TabControl style and also a style for a Grid. The latter is needed because you can't define an event handler within the TabControl style directly.
<Window x:Class="WpfApp7.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:WpfApp7"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<Style x:Key="GridStyle" TargetType="Grid">
<EventSetter Event="Mouse.MouseDown" Handler="UIElement_OnMouseDown"/>
</Style>
<Style x:Key="TabcontrolStyle" TargetType="{x:Type TabControl}">
<Style.Resources>
<Style TargetType="{x:Type TabItem}">
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Grid Height="20"
Background="{TemplateBinding Background}"
SnapsToDevicePixels="true"
Style="{StaticResource GridStyle}">
<ContentPresenter Margin="10,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
ContentSource="Header" >
</ContentPresenter>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="false">
<Setter Property="Background" Value="Transparent" />
</Trigger>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Background" Value="{x:Static SystemColors.ControlBrush}" />
</Trigger>
<Trigger Property="IsSelected" Value="true">
<Setter Property="Background" Value="{x:Static SystemColors.ActiveCaptionBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Style.Resources>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabControl}">
<Grid KeyboardNavigation.TabNavigation="Local">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TabPanel Name="HeaderPanel"
Panel.ZIndex="1"
IsItemsHost="True"
KeyboardNavigation.TabIndex="1" />
<ContentPresenter Name="PART_SelectedContentHost"
Margin="10"
Grid.Row="1"
ContentSource="SelectedContent" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid>
<TabControl x:Name="MyTabControl" Style="{StaticResource TabcontrolStyle}">
<TabControl.ContextMenu>
<ContextMenu Name="tabContextMenu">
<MenuItem Header="Change Tab Name" />
<MenuItem Header="Save Favorite Layers" />
</ContextMenu>
</TabControl.ContextMenu>
<TabItem Header="First">First Content</TabItem>
<TabItem Header="Second">Second Content</TabItem>
<TabItem Header="Third">Third Content</TabItem>
</TabControl>
</Grid>
Code behind
In the code behind you can equals the TabItem header to selected the TabItem accordingly.
private void UIElement_OnMouseDown(object sender, MouseButtonEventArgs e)
{
ContentPresenter contentPresenter = null;
if (e.Source is Grid)
contentPresenter = (ContentPresenter) ((Grid) e.Source).Children[0];
else if (e.Source is ContentPresenter)
contentPresenter = ((ContentPresenter) e.Source);
if (contentPresenter == null) return;
var header = contentPresenter.Content.ToString();
var selectedIndex = -1;
foreach (var item in this.MyTabControl.Items)
{
selectedIndex++;
var tabItem = item as TabItem;
if (tabItem?.Header != null && tabItem.Header.ToString().Equals(header, StringComparison.InvariantCultureIgnoreCase))
{
this.MyTabControl.SelectedIndex = selectedIndex;
}
}
}
I'm making an application using C# and WPF
I have 8 toggle buttons.
When I click one of the buttons others should be disabled so I can't click it except one is activated.
When I click this button again others should be enabled
Styles.xaml:
<!--Toggle Button-->
<Style x:Key="ToggleButton" TargetType="{x:Type ToggleButton}">
<Setter Property="Background" Value="#535353"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="MinHeight" Value="30"/>
<Setter Property="Grid.Column" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border Background="{TemplateBinding Background}"
BorderThickness="0">
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
MainWindow.xaml:
<ToggleButton x:Name="CategoryToggle1"
Grid.Row="1"
Style="{StaticResource ToggleButton}"
Checked="ShowOptions"
Unchecked="HideOptions" />
How Can I achieve this by code and XAML?
I want to do it like in this video:
Video
Thank you for comments guys, I found another solution.
MainWindow.XAML:
<RadioButton x:Name="CategoryToggle1"
Grid.Row="1"
Grid.Column="1"
Style="{StaticResource RadioButton}"
GroupName="ToggleButtonsGroup"
Checked="OpenOptions"
Unchecked="HideOptions"/>
Styles.xaml:
<!--Radio Button-->
<Style TargetType="RadioButton"
x:Key="RadioButton"
BasedOn="{StaticResource {x:Type ToggleButton}}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<ToggleButton IsChecked="{Binding IsChecked, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!--Toggle Button-->
<Style TargetType="{x:Type ToggleButton}">
<Setter Property="Background" Value="#535353" />
<Setter Property="MinHeight" Value="30" />
<Setter Property="Grid.Column" Value="1" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border Background="{TemplateBinding Background}"
BorderThickness="0">
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Opacity" Value="0.5" />
</Trigger>
<Trigger Property="IsEnabled" Value="true">
<Setter Property="Opacity" Value="1" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
MainWindow.cs:
private void OpenOptions(object sender, RoutedEventArgs e){
RadioButton radioButton = sender as RadioButton;
radioButton.IsChecked = true;
//Disable all option buttons except one that active
MyGrid.Children.OfType<RadioButton>().Where(rb => rb != radioButton &&
rb.GroupName == radioButton.GroupName).ToList().ForEach(rb => rb.IsEnabled = false);
}
private void HideOptions(object sender, RoutedEventArgs e)
{
RadioButton radioButton = sender as RadioButton;
MyGrid.Children.OfType<RadioButton>().Where(rb => rb.GroupName ==
radioButton.GroupName).ToList().ForEach(rb => rb.IsEnabled = true);
}
Using Click events of each ToggleButton
One way you could do it is by giving a name to all your ToggleButtons, hook-up to their Click event and manually uncheck others in the code-behind:
XAML
<StackPanel Orientation="Vertical">
<StackPanel.Resources>
<Style TargetType="ToggleButton">
<Style.Triggers>
<Trigger Property="IsChecked" Value="False">
<Setter Property="MaxWidth" Value="15"/>
</Trigger>
</Style.Triggers>
</Style>
</StackPanel.Resources>
<StackPanel Orientation="Horizontal">
<ToggleButton x:Name="button1" Content="Button 1" Click="button1_Click"/>
<TextBlock Text="Line 1"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<ToggleButton x:Name="button2" Content="Button 2" Click="button2_Click"/>
<TextBlock Text="Line 2"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<ToggleButton x:Name="button3" Content="Button 3" Click="button3_Click"/>
<TextBlock Text="Line 3"/>
</StackPanel>
</StackPanel>
Code-behind
private void button1_Click(object sender, RoutedEventArgs e) {
if (button1.IsChecked == true) {
button2.IsChecked = false;
button3.IsChecked = false;
}
}
private void button2_Click(object sender, RoutedEventArgs e) {
if (button2.IsChecked == true) {
button1.IsChecked = false;
button3.IsChecked = false;
}
}
private void button3_Click(object sender, RoutedEventArgs e) {
if (button3.IsChecked == true) {
button1.IsChecked = false;
button2.IsChecked = false;
}
}
This method is tedious, error-prone, requires code-behind and is not very scalable.
Binding IsChecked properties to a collection of bool with one true at a time.
Another way you could go (still by using code-behind) is to define a collection of boolean values and bind each ToggleButton.IsChecked on one of the bool in the collection, and ensure that the collection only contains at most one true at a time:
<StackPanel Orientation="Horizontal">
<ToggleButton x:Name="button1" Content="Button 1" IsChecked="{Binding [0]}"/>
<TextBlock Text="Line 1"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<ToggleButton x:Name="button2" Content="Button 2" IsChecked="{Binding [1]}"/>
<TextBlock Text="Line 2"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<ToggleButton x:Name="button3" Content="Button 3" IsChecked="{Binding [2]}"/>
<TextBlock Text="Line 3"/>
</StackPanel>
Code-behind
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
ObservableCollection<bool> states = new ObservableCollection<bool> { false, false, false };
states.CollectionChanged += States_CollectionChanged;
DataContext = states;
}
private void States_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
var collection = sender as ObservableCollection<bool>;
if (e.Action == NotifyCollectionChangedAction.Replace) {
if ((bool)e.NewItems[0]) {
for (int i = 0; i < collection.Count; i++) {
if (e.NewStartingIndex != i) {
collection[i] = false;
}
}
}
}
}
}
Again, this uses code-behind and not the view model but at least it is easier to add rows.
The behavior you need is very specific.
I don't know how, but i got here trying to make a toggle button behave like a radio button. Your answer was enlightning.
For what it's worth, here's how you would do that :
Resource :
<Style x:Key='RadioToggle' TargetType='RadioButton'
BasedOn='{StaticResource {x:Type ToggleButton}}' />
Control :
<RadioButton Content='RadioToggle1' IsChecked='True'
Style='{StaticResource RadioToggle}'
GroupName="RadioToggleGroup" />
<RadioButton Content='RadioToggle2'
Style='{StaticResource RadioToggle}'
GroupName="RadioToggleGroup" />
<RadioButton Content='RadioToggle3'
Style='{StaticResource RadioToggle}'
GroupName="RadioToggleGroup" />
As topic mentioned.I want to use only one popup for all button in my application.I don't know how to get what I want.
Here is what my window looks like:
Info 1:
Info 2:
You can see popup appear on wrong position.I know I can position a popup by setting the PlacementTarget.But each Popup has a different value for the placement property.That is the problem.I'm looking another way to do it.
Here is a popup for option 1:
<StackPanel Orientation="Horizontal">
<!--Option 1: text and button-->
<TextBlock Text="Option 1"
Margin="10"
VerticalAlignment="Center" />
<Popup x:Name="popInfo"
PlacementTarget="{Binding ElementName=btnInfoOption1}"
IsOpen="{Binding IsShowInfo1}">
<ContentControl Style="{StaticResource ContentInfoStyle}">
<TextBlock Text="{Binding InfoContent}"
TextWrapping="Wrap"
Foreground="White"
Width="340"
Padding="10"
Margin="30,0,30,5"
FontSize="15" />
</ContentControl>
</Popup>
<Button x:Name="btnInfoOption1"
Style="{StaticResource btnIcons}"
Background="#0063b1"
Width="30"
Height="30"
Margin="10,10,20,10"
Command="{Binding CmdShowInfo, Delay=1500}"
Tag="{StaticResource ic_ginfo}" />
</StackPanel>
popup for option 2:
<StackPanel Orientation="Horizontal">
<!--Option 2: text and button-->
<TextBlock Text="Option 2"
Margin="10"
VerticalAlignment="Center" />
<Button x:Name="btnOption2"
Style="{StaticResource btnIcons}"
Background="#0063b1"
Width="30"
Height="30"
Margin="10,10,20,10"
Command="{Binding CmdShowInfo, Delay=1500}"
Tag="{StaticResource ic_ginfo}" />
</StackPanel>
ContentControl Style:
<Style TargetType="{x:Type ContentControl}"
x:Key="ContentInfoStyle">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Border Background="Green"
CornerRadius="3"
Padding="10,0,12,10">
<StackPanel>
<Button HorizontalAlignment="Right"
Tag="{StaticResource ic_gclear}"
Style="{StaticResource btnIcons}"
Background="White"
Margin="10,5,12,5"
Command="{Binding DataContext.CmdCloseInfo}"
Height="24" />
<ContentPresenter x:Name="content"
TextBlock.FontSize="14"
TextBlock.Foreground="White"
TextBlock.FontFamily="Arial"
Content="{TemplateBinding ContentControl.Content}" />
</StackPanel>
</Border>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
Button icon style:
<Style TargetType="Button"
x:Key="btnIcons">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border x:Name="brd" Background="Transparent"
SnapsToDevicePixels="True">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="MouseOver" />
<VisualState x:Name="Pressed" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Path Stretch="Uniform" VerticalAlignment="Center"
Fill="{TemplateBinding Background}"
Data="{TemplateBinding Tag}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
ViewModel.cs:
the content of popup:
private string _InfoContent;
public string InfoContent
{
get { return _InfoContent; }
set
{
if (value != _InfoContent)
{
_InfoContent = value;
OnRaise("InfoContent");
}
}
}
show the popup for option2 and option1:
private bool _IsShowInfo2;
public bool IsShowInfo2
{
get { return _IsShowInfo2; }
set
{
if (value != _IsShowInfo2)
{
_IsShowInfo2 = value;
OnRaise("IsShowInfo2");
}
}
}
//show the popup for option1
private bool _IsShowInfo1;
public bool IsShowInfo1
{
get { return _IsShowInfo1; }
set
{
if (value != _IsShowInfo1)
{
_IsShowInfo1 = value;
OnRaise("IsShowInfo1");
}
}
}
the command for button:
private ICommand _CmdShowInfo;
public ICommand CmdShowInfo
{
get
{
_CmdShowInfo = _CmdShowInfo ?? new RelayCommand(x => this.ShowInfo(true, 1), () => true);
return _CmdShowInfo;
}
}
private ICommand _CmdShowInfo2;
public ICommand CmdShowInfo2
{
get
{
_CmdShowInfo2 = _CmdShowInfo2 ?? new RelayCommand(x => this.ShowInfo(true, 0), () => true);
return _CmdShowInfo2;
}
}
private void ShowInfo(bool show = true, byte option = 0)
{
if (option == 0)
{
this.InfoContent = "Option 1...";
}
else if (option == 1)
{
this.InfoContent = "Option 2...";
}
this.IsShowInfo1 = show;
}
My initial thought was to do this with a styled HeaderedContentControl, but then you've got the icon fill color and the icon data, and I'd have had to add attached properties for those. Once you go there, you may as well just write a custom control.
The dependency properties of IconPopupButton can be bound like any dependency property:
<hec:IconPopupButton
IsOpen="{Binding IsShowInfo1}"
IconFill="YellowGreen"
Content="Another Test Popup"
IconData="M -10,-10 M 0,3 L 17,20 L 20,17 L 3,0 Z M 0,0 L 0,20 L 20,20 L 20,0 Z"
/>
If you want to parameterize the Style applied to the ContentControl in the Popup, add another dependency property. You'll need to give that some thought, though, because you need that ToggleButton to be bound to IsOpen on the templated parent, one way or another. Perhaps you could bind it to the viewmodel property that's bound to the popup button's IsOpen. There's always a way.
So here's that. With snippets to create dependency properties, this is pretty much just a fill-in-the-blanks exercise. Much less to it than meets the eye.
IconPopupButton.cs
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
namespace HollowEarth.Controls
{
public class IconPopupButton : ContentControl
{
static IconPopupButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(IconPopupButton), new FrameworkPropertyMetadata(typeof(IconPopupButton)));
}
#region IconData Property
public Geometry IconData
{
get { return (Geometry)GetValue(IconDataProperty); }
set { SetValue(IconDataProperty, value); }
}
public static readonly DependencyProperty IconDataProperty =
DependencyProperty.Register("IconData", typeof(Geometry), typeof(IconPopupButton),
new PropertyMetadata(null));
#endregion IconData Property
#region IconFill Property
public Brush IconFill
{
get { return (Brush)GetValue(IconFillProperty); }
set { SetValue(IconFillProperty, value); }
}
public static readonly DependencyProperty IconFillProperty =
DependencyProperty.Register("IconFill", typeof(Brush), typeof(IconPopupButton),
new PropertyMetadata(SystemColors.ControlTextBrush));
#endregion IconFill Property
#region IsOpen Property
public bool IsOpen
{
get { return (bool)GetValue(IsOpenProperty); }
set { SetValue(IsOpenProperty, value); }
}
public static readonly DependencyProperty IsOpenProperty =
DependencyProperty.Register("IsOpen", typeof(bool), typeof(IconPopupButton),
new PropertyMetadata(false));
#endregion IsOpen Property
#region StaysOpen Property
public bool StaysOpen
{
get { return (bool)GetValue(StaysOpenProperty); }
set { SetValue(StaysOpenProperty, value); }
}
public static readonly DependencyProperty StaysOpenProperty =
DependencyProperty.Register("StaysOpen", typeof(bool), typeof(IconPopupButton),
new PropertyMetadata(false));
#endregion StaysOpen Property
#region Placement Property
public PlacementMode Placement
{
get { return (PlacementMode)GetValue(PlacementProperty); }
set { SetValue(PlacementProperty, value); }
}
public static readonly DependencyProperty PlacementProperty =
DependencyProperty.Register("Placement", typeof(PlacementMode), typeof(IconPopupButton),
new PropertyMetadata(PlacementMode.Right));
#endregion Placement Property
}
}
Themes\Shared.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:HeaderedPopupTest.Themes"
>
<Geometry x:Key="ic_gclear">M56,4 52,0 28,24 4,0 0,4 24,28 0,52 4,56 28,32 52,56 56,52 32,28Z</Geometry>
<Geometry x:Key="ic_ginfo">M31,0C13.879,0,0,13.879,0,31s13.879,31,31,31s31-13.879,31-31S48.121,0,31,0z M34,46h-6V27.969h6V46z M34,21.969h-6V16h6V21.969z</Geometry>
<Style TargetType="ButtonBase" x:Key="btnIcons">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ButtonBase}">
<Border x:Name="brd" Background="Transparent" SnapsToDevicePixels="True">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="MouseOver" />
<VisualState x:Name="Pressed" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid>
<Path
x:Name="Path"
Stretch="Uniform"
VerticalAlignment="Center"
Fill="{TemplateBinding Background}"
Data="{TemplateBinding Tag}"
/>
<TextBlock
x:Name="MissingIconData"
Visibility="Collapsed"
Text="?"
FontWeight="Bold"
FontSize="30"
ToolTip="IconData (Tag) not set"
HorizontalAlignment="Center"
VerticalAlignment="Center"
/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="Tag" Value="{x:Null}">
<Setter TargetName="MissingIconData" Property="Visibility" Value="Visible" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Themes\Generic.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:HeaderedPopupTest.Themes"
xmlns:hec="clr-namespace:HollowEarth.Controls"
>
<ResourceDictionary.MergedDictionaries>
<!-- Change HeaderedPopupTest to the name of your own assembly -->
<ResourceDictionary Source="/HeaderedPopupTest;component/Themes/Shared.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style TargetType="hec:IconPopupButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="hec:IconPopupButton">
<Grid x:Name="Grid">
<ToggleButton
x:Name="OpenButton"
Style="{StaticResource btnIcons}"
Background="{TemplateBinding IconFill}"
Tag="{TemplateBinding IconData}"
IsChecked="{Binding IsOpen, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
ToolTip="{TemplateBinding ToolTip}"
/>
<Popup
x:Name="Popup"
StaysOpen="{Binding StaysOpen, RelativeSource={RelativeSource TemplatedParent}}"
IsOpen="{Binding IsOpen, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
PlacementTarget="{Binding ElementName=ToggleButton}"
Placement="{TemplateBinding Placement}"
>
<Border
Background="Green"
CornerRadius="3"
Padding="10,0,12,10">
<StackPanel>
<ToggleButton
HorizontalAlignment="Right"
Tag="{StaticResource ic_gclear}"
Style="{StaticResource btnIcons}"
Background="White"
Margin="10,5,12,5"
IsChecked="{Binding IsOpen, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
Height="24"
/>
<ContentPresenter
x:Name="content"
TextBlock.FontSize="14"
TextBlock.Foreground="White"
TextBlock.FontFamily="Arial"
Content="{TemplateBinding Content}"
/>
</StackPanel>
</Border>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<!--
I don't understand this: If I use the templated parent's IsOpen,
the effect is as if it were never true.
-->
<Condition SourceName="Popup" Property="IsOpen" Value="True" />
<Condition Property="StaysOpen" Value="False" />
</MultiTrigger.Conditions>
<!--
If StaysOpen is false and the button is enabled while the popup is open,
then clicking on it will cause the popup to flicker rather than close.
-->
<Setter TargetName="OpenButton" Property="IsEnabled" Value="False" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
MainWindow.xaml example usage:
<Window
x:Class="HeaderedPopupTest.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:HeaderedPopupTest"
xmlns:hec="clr-namespace:HollowEarth.Controls"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
>
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Themes\Shared.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style
x:Key="InfoPopupButton"
TargetType="hec:IconPopupButton"
BasedOn="{StaticResource {x:Type hec:IconPopupButton}}"
>
<Setter Property="IconFill" Value="DeepSkyBlue" />
<Setter Property="IconData" Value="{StaticResource ic_ginfo}" />
</Style>
</ResourceDictionary>
</Window.Resources>
<Grid>
<StackPanel
Orientation="Vertical"
HorizontalAlignment="Left"
>
<hec:IconPopupButton
Style="{StaticResource InfoPopupButton}"
Content="This is a test popup"
ToolTip="Test Popup Tooltip"
/>
<hec:IconPopupButton
IconFill="YellowGreen"
Content="Another Test Popup"
IconData="M -10,-10 M 0,3 L 17,20 L 20,17 L 3,0 Z M 0,0 L 0,20 L 20,20 L 20,0 Z"
/>
<hec:IconPopupButton
IconFill="DarkRed"
Content="Missing IconData behavior example"
/>
</StackPanel>
</Grid>
</Window>
You'll notice I changed your buttons to ToggleButton. This is for convenience in wiring them up to the IsOpen property: With a ToggleButton, I just bind IsChecked and I'm done. No need for commands. One side effect of that is that if StaysOpen is false, then when the user clicks on the open button for a Popup, the focus change closes the Popup, which unchecks the button, and then the button gets the mouse message. So the button opens the popup again. This is bizarre behavior from the user's perspective, so you add a trigger to disable the button when the popup is open and StaysOpen is false. When StaysOpen is true, focus change doesn't close the Popup, so you want the button to be enabled in that case.
I changed the btnIcons style to target ButtonBase, so it works identically with Button and ToggleButton.