Bind Control's Property to Variable, Bind Variable to Another Control - wpf

Its hard to understand something from the Title, but its also hard for me to explain but I will try.
I have custom control to pick color.
The custom control has Property called SelectedColor:
/// <summary>
/// SelectedColor property backing ReadOnly DependencyProperty.
/// </summary>
private static readonly DependencyPropertyKey SelectedColorPropertyKey
= DependencyProperty.RegisterReadOnly("SelectedColor", typeof(Color), typeof(ImageColorPicker)
, new FrameworkPropertyMetadata(Colors.Transparent
, FrameworkPropertyMetadataOptions.AffectsRender));
/// <summary>
/// Gets or sets the color selected.
/// </summary>
/// <value>The color selected.</value>
public Color SelectedColor
{
get { return (Color)GetValue(SelectedColorPropertyKey.DependencyProperty); }
}
I want to bind that Property to a variable I have in my ViewModel which is called TabColor:
public event PropertyChangedEventHandler PropertyChanged; //Event to notify when Property changed.
/// <summary>
/// Notify that Property has Changed.
/// </summary>
/// <param name="propertyName">The name of the Property</param>
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
private Brush _tabColor; //Color of the Tab.
//Return/Set the color of the Tab.
public Brush TabColor
{
get
{
return _tabColor;
}
set
{
_tabColor = value;
NotifyPropertyChanged("TabColor");
}
}
Now, I want to bind the TabColor to Properties on my Style:
<Style TargetType="{x:Type MetroStyle:MetroTabItem}">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="Header" Value="{Binding TabTitle}" />
<Setter Property="Foreground" Value="{Binding TabColor}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type MetroStyle:MetroTabItem}">
<Grid Height="26" Background="{TemplateBinding Background}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter Margin="5,0" HorizontalAlignment="Left" VerticalAlignment="Center" ContentSource="Header">
<ContentPresenter.Resources>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type MetroStyle:MetroTabItem}}}" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</ContentPresenter.Resources>
</ContentPresenter>
<StackPanel Grid.Column="1" Height="16" Margin="0,0,1,0" HorizontalAlignment="Center" VerticalAlignment="Center" Orientation="Horizontal">
<Button Width="16" Click="ShowHide_Click" Content="🌕" Style="{StaticResource CustomizedMetroTabItemButton}" ToolTip="Hide" Visibility="{Binding WindowsVisible, Converter={StaticResource BooleanToVisibilityConverter}}" />
<Button Width="16" Click="ShowHide_Click" Content="🌑" Style="{StaticResource CustomizedMetroTabItemButton}" ToolTip="Show" Visibility="{Binding WindowsVisible, Converter={StaticResource InverseBooleanToVisibilityConverter}}" />
<Button Width="16" Click="Clear_Click" Content="" Style="{StaticResource CustomizedMetroTabItemButton}" ToolTip="Clear" />
<ToggleButton Width="16" x:Name="Edit" Content="î„„" Style="{StaticResource CustomizedMetroTabItemToggleButton}" ToolTip="Edit" />
<Popup IsOpen="{Binding IsChecked, Mode=TwoWay, ElementName=Edit}" StaysOpen="False" PlacementTarget="{Binding ElementName=Edit}" Placement="Left" VerticalOffset="{Binding ActualHeight, ElementName=Edit}" HorizontalOffset="{Binding Width, ElementName=Edit}" PopupAnimation="Slide">
<StackPanel>
<local:ImageColorPicker x:Name="ColorPicker" Source="Images/ColorWheel.png" HorizontalAlignment="Center" Width="100" Height="100"/>
</StackPanel>
</Popup>
</StackPanel>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="False">
<Setter Property="Background" Value="#EFEFF2" />
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#FFFFFF" />
<Setter Property="Foreground" Value="{Binding TabColor}" />
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{Binding TabColor}" />
<Setter Property="Foreground" Value="#FFFFFF" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The reason why I use the variable as a "connector" is because I cant bind Properties out of the Tree - right? (I mean I cant do this: <Setter Property="Foreground" Value="{Binding ElementName=ColorPicker, Path=SelectedColor, Converter={StaticResource ColorToBrushConverter}}" />
I also have an Image that should simplify what I am asking:
If you have another solution as long as I will achieve what I need - I will accept.
Thanks!
EDIT:
I have a converter which converts from Color to SolidColorBrush but still, I dont know how to do the binding I want:
[ValueConversion(typeof(Color), typeof(Brush))]
public class ColorToBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Color color = (Color)value;
return new SolidColorBrush(color);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
EDIT2:
Sample code with everything above
EDIT3:
A new picture (maybe it will be more understandable)

I believe all you need to do is set up a binding on Selected Color, something like:
<local:ImageColorPicker x:Name="ColorPicker" Source="Images/ColorWheel.png" HorizontalAlignment="Center" Width="100" Height="100" SelectedColor="{Binding Path=TabColor, Converter={StaticResource ColorToBrushConverter}}/>
You can always set a binding on a dependency property (but nothing else). Hopefully that clears up "Binding properties out of the tree". If not, please clarify what you don't understand and I would be happy to try and address it.
Also, the dependency property isn't set up how I am used to seeing it, so if you are still running into trouble, you might look into setting it up with the dpprop code snippet, or follow the example on MSDN.
Comments
Bindings have multiple modes, and if you specifically want just one you just set the mode variable. In your case, you would change the binding to:
SelectedColor="{Binding Path=TabColor, Converter={StaticResource ColorToBrushConverter, Mode=OneWay}}
That way, any changes to the "TabColor" property won't propagate to the ColorPicker, but changes from the ColorPicker will set the "TabColor" property. For reference the other modes are: TwoWay (the default), OneTime (get the default just on initialization), and OneWayToSource(like OneWay, but it only changes the source, and never picks up updates).
When I mentioned the "unusual" dependency property, I was referring to the style of the "SelectedColor" DP. Of course TabColor will not be a DP :).
Finally, I believe that you can bind the Value property as described. Have you tried it? The only restriction on binding is that it must be done on a DP.
Update
I could not get the ColorPicker to update the view model. Everything I've read indicates that it should be able to, but I wasn't able to figure it out. If that is an important requirement, I would suggest starting from scratch and get just that working, then keep going.
That being said, I did get the color to update. Try changing your XAML to:
<TabItem Header="Test" Foreground="{Binding ElementName=ColorPicker, Path=SelectedColor, Converter={StaticResource ColorToBrushConverter}, Mode=OneWay}">
<Grid>
<local:ImageColorPicker x:Name="ColorPicker" Source="ColorWheel.png" HorizontalAlignment="Center"/>
</Grid>
</TabItem>
For some reason, I had to rebuild a couple times to make this work. Again, I'm sorry I couldn't get the original request working.
Update 2
Apparently all you need to do is add "BindsTwoWayByDefault" to the metadata options. This will cause the "ConvertBack" function to be called since you are updating the ViewModel.
You could probably get away with having "OneWay" bindings on all the controls EXCEPT the ImageColorPicker so that they don't accidentally set the color, but in general TwoWay should be fine.

Related

WPF trigger on attached property not working

i'm trying to change an image which i put as button's content through an attached property, yet for some reason the image is not changing and i don't get any exceptions or anything solid in the output.
Here is my attached property class code:
public class ImageButton
{
#region Image dependency property
/// <summary>
/// An attached dependency property which provides an
/// <see cref="ImageSource" /> for arbitrary WPF elements.
/// </summary>
public static DependencyProperty ImageProperty;
/// <summary>
/// Gets the <see cref="ImageProperty"/> for a given
/// <see cref="DependencyObject"/>, which provides an
/// <see cref="ImageSource" /> for arbitrary WPF elements.
/// </summary>
public static ImageSource GetImage(DependencyObject obj)
{
return (ImageSource)obj.GetValue(ImageProperty);
}
/// <summary>
/// Sets the attached <see cref="ImageProperty"/> for a given
/// <see cref="DependencyObject"/>, which provides an
/// <see cref="ImageSource" /> for arbitrary WPF elements.
/// </summary>
public static void SetImage(DependencyObject obj, ImageSource value)
{
obj.SetValue(ImageProperty, value);
}
#endregion
static ImageButton()
{
//register attached dependency property
var metadataImage = new FrameworkPropertyMetadata((ImageSource)null);
ImageProperty = DependencyProperty.RegisterAttached("Image",
typeof(ImageSource),
typeof(ImageButton), metadataImage);
}
}
here the code for the style using the attached property(the image source is set to take the value):
<Style TargetType="{x:Type Button}" x:Key="MyImageButton">
<Setter Property="Background" Value="White"/>
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border
x:Name="Border"
Background="White">
<ContentPresenter>
<ContentPresenter.Content>
<Grid
x:Name="Grid"
Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Image
Grid.Column="0"
Source="{Binding Path=(attachedProperties:ImageButton.Image),
RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type Button}}}"
Width="30"
Height="25"
Margin="5,0,2,0"
VerticalAlignment="Center"/>
<TextBlock
x:Name="TextBlock"
Grid.Column="1"
Text="{Binding Path=(attachedProperties:ImageButton.StringContent),
RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type Button}}}"
VerticalAlignment="Center"
TextAlignment="Center"
Margin="0,0,15,0"/>
</Grid>
</ContentPresenter.Content>
</ContentPresenter>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
and finally the XAML code using the style and the trigger there trying to change the image source:
<Button
Grid.Row="2"
Width="30"
attachedProperties:ImageButton.Image="..\Images\Down_Arrow_Enabled_Icon_25x25.png"
Command="{Binding PriorityDownCommand}">
<Button.Style>
<Style TargetType="{x:Type Button}"
BasedOn="{StaticResource MyImageButton}">
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="attachedProperties:ImageButton.Image" Value="..\Images\Down_Arrow_Disabled_Icon_25x25.png"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
thanks ahead for anyone who can find the problem!!!
Please take a look at this Dependency Property Value Precedence article.
Your problem is that you are trying to override a Local Property Value from withing a Style Trigger, this cannot be done since the Local Value has a higher precedence value.
You should set the default property in style setters:
<Button
Grid.Row="2"
Width="30"
Command="{Binding PriorityDownCommand}">
<Button.Style>
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource MyImageButton}">
<Setter Property="attachedProperties:ImageButton.Image" Value="..\Images\Down_Arrow_Enabled_Icon_25x25.png"/>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="attachedProperties:ImageButton.Image" Value="..\Images\Down_Arrow_Disabled_Icon_25x25.png"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>

Page indicator using ItemsControl ignores data template

I'm trying to create a page indicator using ItemsControl. The idea is to bind to the tabs of a TabControl and display a circle for each tab, with the color determined by a trigger that checks whether the tab is selected or not:
<ItemsControl ItemsSource="{Binding Path=Items, ElementName=ATabControl}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type TabItem}">
<Ellipse x:Name="PageIndicator" Width="6" Height="6" Margin="2,0" />
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=IsSelected}" Value="False">
<Setter TargetName="PageIndicator" Property="Fill" Value="White" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=IsSelected}" Value="True">
<Setter TargetName="PageIndicator" Property="Fill" Value="Blue" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
It compiles without error but instead of the circles, I get the names of the tabs listed. Practically, it ignores the ItemTemplate/DataTemplate completely. Actually, if I remove the latter, the display remains the same all right.
There is always an issue while binding UI elements in two different containers. as one UI element can have only 1 parent so the last parent will have the actual element and hence the same will be removed from the former parent containers.
in the issue you've mentioned you attempted to bind the UI element TabItems to the items control which effectively pulled the original elements from the tab control and placed them as child of the items control.
in order to solve this issue I propose a solution to wrap such UI elements in a class and wire the properties needed.
I attempted a solution using converter
xaml
<StackPanel xmlns:l="clr-namespace:CSharpWPF">
<StackPanel.Resources>
<l:TabItemsConverter x:Key="TabItemsConverter" />
</StackPanel.Resources>
<TabControl x:Name="ATabControl">
<TabItem Header="item 1" />
<TabItem Header="item 2" />
<TabItem Header="item 3" />
</TabControl>
<ItemsControl ItemsSource="{Binding Path=Items, ElementName=ATabControl,Converter={StaticResource TabItemsConverter}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse x:Name="PageIndicator"
Width="6"
Height="6"
Margin="2,0"
Fill="Gray" />
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=IsSelected}"
Value="False">
<Setter TargetName="PageIndicator"
Property="Fill"
Value="Gray" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=IsSelected}"
Value="True">
<Setter TargetName="PageIndicator"
Property="Fill"
Value="Blue" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
converter class
namespace CSharpWPF
{
public class TabItemsConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
List<TabItemWrapper> result = new List<TabItemWrapper>();
foreach (TabItem item in (ItemCollection)value)
{
result.Add(new TabItemWrapper(item));
}
return result;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
class TabItemWrapper : DependencyObject
{
public bool IsSelected
{
get { return (bool)GetValue(IsSelectedProperty); }
set { SetValue(IsSelectedProperty, value); }
}
// Using a DependencyProperty as the backing store for IsSelected. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsSelectedProperty =
DependencyProperty.Register("IsSelected", typeof(bool), typeof(TabItemWrapper), new PropertyMetadata(false));
public TabItemWrapper(TabItem source)
{
Binding b = new Binding("IsSelected");
b.Source = this;
b.Mode = BindingMode.TwoWay;
source.SetBinding(TabItem.IsSelectedProperty, b);
}
}
}
}
in this converter I binded the tab item's IsSelected to the property of a wrapper class and used it to bind in the view
Note: this converter work for static tab items only, if you intend to add or remove the tab items during runtime then perhaps you may need to handle CollectionChanged events to keep the result in sync.
result

Access Button's Tag property from ControlTemplate in XAML

In my Window I have a series of six buttons that indicate the six possible states for one of the properties of my ViewModel. The one that's active needs to be highlighted. To do this, I've created the following ControlTemplate for the buttons:
<ControlTemplate x:Key="SnijRichtingTemplate" TargetType="Button">
<Border Name="toggleButton" BorderThickness="1" BorderBrush="{StaticResource KleurRadioCheckOuter}" Background="Transparent" Width="20" Height="20" Cursor="Hand">
<TextBlock Name="text" Foreground="{StaticResource KleurRadioCheckOuter}"
Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Tag}"
ToolTip="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Tag.ToolTip}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource EqualityToBooleanConverter}">
<Binding Path="SnijRichting" />
<Binding Path="Tag" RelativeSource="{RelativeSource TemplatedParent}" />
</MultiBinding>
</DataTrigger.Binding>
<Setter TargetName="toggleButton" Property="BorderBrush" Value="{StaticResource KleurTekstDonker}" />
<Setter TargetName="text" Property="Foreground" Value="{StaticResource KleurTekstDonker}" />
</DataTrigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="toggleButton" Property="BorderBrush" Value="{StaticResource Kleur2}" />
<Setter TargetName="text" Property="Foreground" Value="{StaticResource Kleur2}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
The template is then used like so:
<Button Grid.Column="0" Template="{StaticResource SnijRichtingTemplate}"
HorizontalAlignment="Right" Click="SnijRichting_Click"
Tag="{StaticResource XLinks}" />
Where the tag is just an instance defined in the XAML:
<wg:SnijRichting x:Key="XLinks" SnijAs="X" Negatief="True" />
The MultibindingConverter is nothing fancy:
public class EqualityToBooleanConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return values[0] == values[1];
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Basically, each button has a Tag with the new value. In the click handler, the ViewModel's property is set to the button's Tag. The button state is updated by checking whether the button's Tag is equal to the ViewModel property.
The problem is that this doesn't work. When the EqualityToBooleanConverter is executed, the second value is null. By removing the Path="Tag" bit from the second binding I see that the TemplatedParent is a ContentPresenter rather than the Button I was expecting, which explains why Tag is null. Now of course I could write a ValueConverter to get the correct value using VisualTreeHelper.GetParent to get the ContentPresenter's parent (which returns the desired Button), but surely there must be a way to do this from XAML? The obvious Path="Parent.Tag" doesn't work, since the Parent of the ContentPresenter is apparently a Border.
Does anyone know how to access the button's Tag property from XAML?
Found the problem. Turns out you need {RelativeSource Mode=Self}, not {RelativeSource TemplatedParent}.

AutoHide Progressbar with StyleTriggers

I would like to hide a progressbar in WPF using databinding. Whenever a property is 0, the progressbar should hide: I try the following code
(Info: My current datacontext is a class that holds an integer property 'CurrentIndex')
<ProgressBar Minimum="0" Maximum="100" Value="{Binding CurrentIndex, UpdateSourceTrigger=PropertyChanged}" Visibility="Visible">
<ProgressBar.Style>
<Style TargetType="{x:Type ProgressBar}">
<Style.Triggers>
<DataTrigger Binding="{Binding CurrentIndex}" Value="0">
<Setter Property="Visibility" Value="Hidden"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ProgressBar.Style>
</ProgressBar>
What is wrong with this code? Why does the progressbar still show up when the CurrentIndex is 0? (in the model behind, the value of 'CurrentIndex' is 0 by default, when the control is loaded)
DP precedence, do not set Visibility on the control itself (local value > style).
Other way to use visibility binding and a converter:
<Grid>
<Grid.Resources>
<App:VisibilityConverter x:Key="VisibilityConverter" />
</Grid.Resources>
<ProgressBar Minimum="0" Maximum="100" Value="{Binding CurrentIndex, UpdateSourceTrigger=PropertyChanged}"
Visibility="{Binding CurrentIndex, Mode=OneWay, Converter={StaticResource VisibilityConverter}}" />
</Grid>
The converter code (VisibilityConverter.cs):
public class VisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (int)value == 0 ? Visibility.Hidden : Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Your XAML was almost right!
Define your progressbar as you did:
<ProgressBar Minimum="0"
Maximum="100"
Value="{Binding CurrentIndex, UpdateSourceTrigger=PropertyChanged}"
Name="MyAutoHidingProgressBar" />
Don't forget to add the Name property AND do not set the Visibility here.
It will always override what is set in your Style.
Then define a Style as normal in your <Window.Resources>
<Window.Resources>
<Style TargetType="ProgressBar" x:Key="MyAutoHidingProgressBarStyle">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=MyAutoHidingProgressBar, Path=Value}" Value="0">
<Setter Property="Visibility" Value="Hidden"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
What this is basically doing is to check the Value of the progressbar itself, rather than your binding.
As a last step add the Style to your progressbar:
Style="{StaticResource MyAutoHidingProgressBarStyle}"
Now your ProgressBar will auto hide if its Value is 0.
You can also easily add a Trigger to hide it if its full.

Data bound radio button list in WPF

I have a list of options in a data object, and I want to make the equivalent of a radio button list to allow the user to select one and only one of them. Functionality similar to a databound combo box, but in radio button format.
Silly me, I thought this would be built in, but no. How do you do it?
Basically, after reviewing the google results, I started with the info from an MSDN discussion thread where Dr. WPF provided an answer, which talks about styling a ListBox to look right. However, when the listbox is disabled, the background was an annoying color that I couldn't get rid of for the life of me, until I read the MSDN example of the ListBox ControlTemplate, which shows the secret Border element that was kicking my background butt.
So, the final answer here was this style:
<Style x:Key="RadioButtonList" TargetType="{x:Type ListBox}">
<!-- ControlTemplate taken from MSDN http://msdn.microsoft.com/en-us/library/ms754242.aspx -->
<Setter Property="SnapsToDevicePixels" Value="true"/>
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
<Setter Property="ScrollViewer.CanContentScroll" Value="true"/>
<Setter Property="MinWidth" Value="120"/>
<Setter Property="MinHeight" Value="95"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBox">
<Border Name="Border" Background="Transparent"
BorderBrush="Transparent"
BorderThickness="0"
CornerRadius="2">
<ScrollViewer Margin="0" Focusable="false">
<StackPanel Margin="2" IsItemsHost="True" />
</ScrollViewer>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter TargetName="Border" Property="Background"
Value="Transparent" />
<Setter TargetName="Border" Property="BorderBrush"
Value="Transparent" />
</Trigger>
<Trigger Property="IsGrouping" Value="true">
<Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="{x:Type ListBoxItem}" >
<Setter Property="Margin" Value="2" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border Name="theBorder" Background="Transparent">
<RadioButton Focusable="False" IsHitTestVisible="False"
IsChecked="{TemplateBinding IsSelected}">
<ContentPresenter />
</RadioButton>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
</Style>
Which provides a ControlTemplate for, and styles, the ListBox and the Items. And it gets used like this:
<ListBox Grid.Column="1" Grid.Row="0" x:Name="TurnChargeBasedOnSelector" Background="Transparent"
IsEnabled="{Binding Path=IsEditing}"
Style="{StaticResource RadioButtonList}"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:MainForm}}, Path=DataContext.RampTurnsBasedOnList}"
DisplayMemberPath="Description" SelectedValuePath="RampTurnsBasedOnID"
SelectedValue="{Binding Path=RampTurnsBasedOnID, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, ValidatesOnExceptions=True}"/>
The more I spend time with WPF, the more I think it makes the trivial insanely difficult and the insanely difficult trivial. Enjoy. -Scott
Super Simple, MVVM friendly, leveraging DataTemplates for types.
XAML:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate DataType="{x:Type local:Option}">
<RadioButton Focusable="False"
IsHitTestVisible="False"
Content="{Binding Display}"
IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListBoxItem}}">
</RadioButton>
</DataTemplate>
</Window.Resources>
<Grid>
<ListBox ItemsSource="{Binding Options}" SelectedItem="{Binding SelectedOption}"/>
</Grid>
View Model, etc:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new Vm();
}
}
public class Vm
{
public Option[] Options { get { return new Option[] {
new Option() { Display = "A" },
new Option() { Display = "B" },
new Option() { Display = "C" } }; } }
public Option SelectedOption { get; set; }
}
public class Option
{
public string Display { get; set; }
}
If you wrap your option into a specific type (or likely it is already). You can just set a DataTemplate for that type, WPF will automatically use it. (Define DataTemplate in ListBox resources to limit the scope of where the DataTemplate will be applied).
Also use group name in the DataTemplate to set the group if you want.
This is much simpler than changing the control template, however it does mean that you get a blue line on selected items. (Again, nothing a bit of styling can't fix).
WPF is simple when you know how.
Bind the listbox to the ItemsSource of a ListBox with a list of objects that have a property Name (this can change)
<ListBox Name="RadioButtonList">
<ListBox.ItemTemplate >
<DataTemplate >
<RadioButton GroupName="radioList" Tag="{Binding}" Content="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
important GroupName="radioList"
I've done this through a ValueConverter that converts an enum to a bool. By passing the enum value that your radio button represents as the ConverterParameter, the converter returns whether this radio button should be checked or not.
<Window.Resources>
<Converters:EnumConverter x:Key="EnumConverter" />
</Window.Resources>
<RadioButton IsChecked="{Binding Path=MyEnum, Mode=TwoWay,
Converter={StaticResource EnumConverter},
ConverterParameter=Enum1}"}
Content="Enum 1" />
<RadioButton IsChecked="{Binding Path=MyEnum, Mode=TwoWay,
Converter={StaticResource EnumConverter},
ConverterParameter=Enum2}"}
Content="Enum 2" />
EnumConverter is defined as follows:
public class EnumConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType.IsAssignableFrom(typeof(Boolean)) && targetType.IsAssignableFrom(typeof(String)))
throw new ArgumentException("EnumConverter can only convert to boolean or string.");
if (targetType == typeof(String))
return value.ToString();
return String.Compare(value.ToString(), (String)parameter, StringComparison.InvariantCultureIgnoreCase) == 0;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType.IsAssignableFrom(typeof(Boolean)) && targetType.IsAssignableFrom(typeof(String)))
throw new ArgumentException("EnumConverter can only convert back value from a string or a boolean.");
if (!targetType.IsEnum)
throw new ArgumentException("EnumConverter can only convert value to an Enum Type.");
if (value.GetType() == typeof(String))
{
return Enum.Parse(targetType, (String)value, true);
}
//We have a boolean, as for binding to a checkbox. we use parameter
if ((Boolean)value)
return Enum.Parse(targetType, (String)parameter, true);
return null;
}
}
Note that I don't databind to the list of enums to generate the radio buttons, I've done them by hand. If you wanted to fill the list of radio buttons through a binding, I think you'll need to change the IsChecked binding to a MultiBinding which binds to both the current value and the radio's enum value, because you cannot use a binding on ConverterParameter.
Sorry, I'd like to put this response to Scott O's post as a comment on his post, but I do not yet have the reputation to do that. I really liked his answer as it was a style-only solution and hence didn't require any added code-behind or creating a custom-control, etc.
However, I did have one issue when I then went to try using controls inside the ListBoxItems. When I use this style I am unable to focus any of the contained controls due to this line:
<RadioButton Focusable="False"
IsHitTestVisible="False"
IsChecked="{TemplateBinding IsSelected}">
The radio button needs to turn off Focusable and IsHitTestVisible for the IsChecked binding to work correctly. To get around this, I changed the IsChecked from a TemplateBinding to a regular binding, which allowed me to make it a two-way binding. Removing the offending settings gave me this line:
<RadioButton IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsSelected, Mode=TwoWay}">
Which now allows me to focus any controls contained in ListBoxItems as expected.
Hope this helps.
I took my inspiration from Jon Benson's blog entry, but modified his solution to use enumerations that have a description attribute. So the key parts of the solution became:
Enumerator with descriptions
public enum AgeRange {
[Description("0 - 18 years")]
Youth,
[Description("18 - 65 years")]
Adult,
[Description("65+ years")]
Senior,
}
Code for reading descriptions and returning key/value pairs for binding.
public static class EnumHelper
{
public static string ToDescriptionString(this Enum val)
{
var attribute =
(DescriptionAttribute)
val.GetType().GetField(val.ToString()).GetCustomAttributes(typeof(DescriptionAttribute), false).
SingleOrDefault();
return attribute == default(DescriptionAttribute) ? val.ToString() : attribute.Description;
}
public static List<KeyValuePair<string,string>> GetEnumValueDescriptionPairs(Type enumType)
{
return Enum.GetValues(enumType)
.Cast<Enum>()
.Select(e => new KeyValuePair<string, string>(e.ToString(), e.ToDescriptionString()))
.ToList();
}
}
Your Object Data Provider in XAML
<ObjectDataProvider
ObjectType="{x:Type local:EnumHelper}"
MethodName="GetEnumValueDescriptionPairs"
x:Key="AgeRanges">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="local:AgeRange" />
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
Your ListBox in XAML
<ListBox
ItemsSource="{Binding Source={StaticResource AgeRanges}}"
SelectedValue="{Binding SelectedAgeRange}"
SelectedValuePath="Key">
<ListBox.ItemTemplate>
<DataTemplate>
<RadioButton
IsChecked="{Binding IsSelected, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}}"
Content="{Binding Value}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The property (e.g. in your view model) that you are binding to
public class YourViewModel : INotifyPropertyChanged
{
private AgeRange _selectedAgeRange;
public AgeRange SelectedAgeRange
{
get { return _selectedAgeRange; }
set
{
if (value != _selectedAgeRange)
{
_selectedAgeRange = value;
OnPropertyChanged("SelectedAgeRange");
}
}
}
}
I cheated:
My solution was to bind the list box programaticly since that is all that seemed to work for me:
if (mUdData.Telephony.PhoneLst != null)
{
lbPhone.ItemsSource = mUdData.Telephony.PhoneLst;
lbPhone.SelectedValuePath = "ID";
lbPhone.SelectedValue = mUdData.Telephony.PrimaryFaxID;
}
The XAML looks like this:
<ListBox.ItemTemplate >
<DataTemplate >
<Grid>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<RadioButton
IsChecked="{Binding Path=PrimaryPhoneID}"
GroupName="Phone"
x:Name="rbPhone"
Content="{Binding Path=PrimaryPhoneID}"
Checked="rbPhone_Checked"/>
<CheckBox Grid.Column="2" IsEnabled="False" IsChecked="{Binding Path=Active}" Content="{Binding Path=Number}" ></CheckBox>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
And in my event to read the value of the radio button as it is selected looks like this:
private void rbPhone_Checked(object sender, RoutedEventArgs e)
{
DataRowView dvFromControl = null;
dvFromControl = (DataRowView)((RadioButton)sender).DataContext;
BindData.Telephony.PrimaryPhoneID = (int)dvFromControl["ID"];
}
Hope that helps someone.

Resources