Creating button which reacts to a dynamic resource (style for dark and light themes) is done like this:
<Button>
<Image Source="{DynamicResource IconId_12}" />
</Button>
The difficulty comes about when attempting the same concept for an ItemsControl of buttons with different icons for each button, each which have a key which refers to either a dark or light themed image source:
<ItemsControl ItemsSource="{Binding ButtonVMs}">
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type ButtonVM}">
<Button Command="{Binding ClickCommand}">
<Image Source="{DynamicResource {Binding IconKey}}" />
</Button>
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
Where ButtonVM looks like this:
public class ButtonVM {
public Command ClickCommand { get; set; }
public string IconKey { get; set; }
}
How can I accomplish binding the resource key name into the dynamic binding?
I have noted that in code you can use <FrameworkElement>.SetResourceReference(SourceProperty, "IconKey"). (as suggested in this stackoverflow answer). But the problem here is that the VM is not a FrameworkElement.
Using a multi-value converter, I was able to access the FrameworkElement (Image) and utilize .SetResourceReferece() to achieve the effect I needed.
public class ImageSourceKeyToDynamicResourceConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var image = (Image)values[0];
var key = (string)values[1];
image.SetResourceReference(Image.SourceProperty, key);
return frameworkElement.GetValue(Image.SourceProperty);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
The xaml sideof things:
<Image>
<Image.Source>
<MultiBinding Converter="{StaticResource KeyToDynamicResourceConverter}">
<Binding RelativeSource="{RelativeSource Self}" />
<Binding Path="IconKey" Mode="OneWay" />
</MultiBinding>
</Image.Source>
</Image>
Related
I try to bind string (which is a path to png) in ViewModel to grid background. This string can be null. It works good but in output cmd is
System.Windows.Data Error: 23 : Cannot convert '' from type '' to type 'System.Windows.Media.ImageSource' for 'en-US' culture with default conversions; consider using Converter
How I can bind a string path to the background to avoid this error?
My current binding:
FieldControl.CS:
public string BackgroundPath
{
get
{
return (string)GetValue(BackgroundPathProperty);
}
set
{
SetValue(BackgroundPathProperty, value);
}
}
public static readonly DependencyProperty BackgroundPathProperty =
DependencyProperty.Register("BackgroundPath", typeof(string), typeof(FieldControl), new PropertyMetadata(null));
FieldControl.XAML:
<Grid.Background>
<ImageBrush
ImageSource="{
Binding Path=BackgroundPath,
Mode=OneWay,
UpdateSourceTrigger=PropertyChanged,
RelativeSource={
RelativeSource Mode=FindAncestor,
AncestorType={x:Type UserControl}
}
}" />
</Grid.Background>
And in MainWindow.XAML:
<ItemsControl
ItemsSource="{Binding FieldsVM}">
...
<ItemsControl.ItemTemplate>
<DataTemplate>
<controls:FieldControl
BackgroundPath="{Binding Path=BackgroundPath }"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
One way to remove the error from the compiler, is to create an IValueConverter and using in the ImageBrush element, as the compiler suggest.
The IValueConverter could be writing like this:
[ValueConversion(typeof(string), typeof(ImageSource))]
public class StringToImageSourceConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
if (value == null) return null;
var path = value.ToString();
return new BitmapImage(new Uri(path, UriKind.RelativeOrAbsolute));
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotSupportedException();
}
}
and in the FieldControl's xaml:
<UserControl.Resources>
<local:StringToImageSourceConverter x:Key="StringToImageSourceConverter" />
</UserControl.Resources>
<Grid Height="100">
<Grid.Background>
<ImageBrush ImageSource="{Binding Path=BackgroundPath, Mode=OneWay, UpdateSourceTrigger=PropertyChanged, ElementName=Uc1,
Converter={StaticResource StringToImageSourceConverter}}" />
</Grid.Background>
</Grid>
After this, you shouldn't see the error from the compiler.
I test the code in this project.
I have a tabcontrol like this:
<TabControl>
<local:TabItem x:Name="son" Header="son">
<local_son:SonView />
</local:TabItem>
<local:TabItem x:Name="daughter" Header="daughter">
<local_daughter:DaughterView />
</local:TabItem>
</TabControl>
There is a button in DaughterView, I want to click this button to switch to
son tab. My questions is how can I reach to tabindex of son tab in the DaughterView?
Thank you in advance!
Seems kinda odd to have a button inside of a tab switch to another tab. I would think that would be a toolbar button or something like that. I.e. outside of the tab. But if you insist :)... I'd use the messenger / event aggregator pattern and post an event and have the view subscribe and switch the tab. I wouldn't have the child view do it byitself.
You need to bind "SelectedIndex" to a property in your view model. I personally like keeping things type-safe and able to be unit-tested, so when I need to manipulate TabControls in code I usually start by declaring an enum with one value for each tab:
public enum MyTabs : int
{
[Description("Tab 1")]
Tab1,
[Description("Tab 2")]
Tab2,
[Description("Tab 3")]
Tab3
}
The description attribute is the text I want displayed in the tab header, more on that in a moment. My view model contains a member of type MyTabs which is updated whenever the user clicks a tab and which I can also set manually myself via code:
public class MyViewModel : ViewModelBase
{
private MyTabs _CurrentTab;
public MyTabs CurrentTab
{
get { return this._CurrentTab;}
set { this._CurrentTab = value; RaisePropertyChanged(() => this.CurrentTab); }
}
}
Now you need to bind your TabControl to this property:
<TabControl
ItemsSource="{Binding Source={StaticResource MyTabs}}"
SelectedIndex="{Binding CurrentTab, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<TabControl.Resources>
<Style TargetType="{x:Type TabItem}">
<Setter Property="Header" Value="{Binding Path=., Converter={StaticResource EnumDescriptionConverter}}" />
</Style>
</TabControl.Resources>
</TabControl>
Unfortunately WPF binding isn't smart enough to work with integer enums, so I'm also using a converter to cast between enums and integers:
public class EnumToIntConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return (int)value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return Enum.ToObject(targetType, value);
}
}
There are a few other things going on here...first of all you'll notice that I'm not actually declaring the TabItems anywhere. That's because I'm generating them automatically from the Enum values themselves; if I add a new value to the MyTab enum then a tab for it magically appears! In this case I'm binding to a static resource with the key "MyTabs", that's a ObjectDataProvider that enumerates the values in my enum:
<ObjectDataProvider x:Key="MyTabs" MethodName="GetValues" ObjectType="{x:Type sys:Enum}">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="local:MyTabs"/>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
This raises the question of how the tabs know what to display in their headers and on the tabitem content areas. The headers use the "Description" attribute declared in the enum, the code for the EnumDescriptionConverter is on another page on this site. To specify the content for each page I create a ControlTemplate for each of my enum values and key it to the enum value itself. A data template is then used to select the appropriate one to use for each tab:
<Window.Resources>
<ControlTemplate x:Key="{x:Static local:MyTabs.Tab1}">
<TextBlock Text="This is the first tab" />
</ControlTemplate>
<ControlTemplate x:Key="{x:Static local:MyTabs.Tab2}">
<TextBlock Text="This is the second tab" />
</ControlTemplate>
<ControlTemplate x:Key="{x:Static local:MyTabs.Tab3}">
<TextBlock Text="This is the third tab" />
</ControlTemplate>
<DataTemplate DataType="{x:Type local:MyTabs}">
<ContentControl>
<ContentControl.Template>
<MultiBinding Converter="{StaticResource ResourceKey=BindingToResourceConverter}">
<Binding RelativeSource="{RelativeSource AncestorType={x:Type Window}}" Path="Resources" />
<Binding />
</MultiBinding>
</ContentControl.Template>
</ContentControl>
</DataTemplate>
</Window.Resources>
The final piece of the puzzle is the BindingToResourceConverter which simply takes a binding (i.e. one of the enum values) and uses it to look up the appropriate ControlTemplate:
public class BindingToResourceConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return (values[0] as ResourceDictionary)[values[1]];
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
And that's it! Every time I want to add a new page to a TabControl I simply add a value to it's corresponding enum and create a ContentControl key'd to that value. Everything else happens automatically, and best of all it's both type-safe and unit-testable.
I'm trying to pass in an array of system:Object to my converter as a parameter:
Xaml: Doesn't work
<TextBlock.Text>
<Binding ElementName="MainGrid" Path="DataContext" Converter="{StaticResource TestConverter}">
<Binding.ConverterParameter>
<x:Array Type="system:Object">
<Binding ElementName="MainGrid" Path="DataContext" />
<Binding ElementName="SomeOtherElement" Path="DataContext" />
</x:Array>
</Binding.ConverterParameter>
</Binding>
</TextBlock.Text>
Following XAML Does work, I found a sample online that used an array of Brush:
<TextBlock.Text>
<Binding ElementName="MainGrid" Path="DataContext" Converter="{StaticResource TestConverter}">
<Binding.ConverterParameter>
<x:Array Type="Brush">
<SolidColorBrush Color="LawnGreen"/>
<SolidColorBrush Color="LightSkyBlue"/>
<SolidColorBrush Color="LightCoral"/>
</x:Array>
</Binding.ConverterParameter>
</Binding>
</TextBlock.Text>
I get a System.Windows.Markup.XamlParseException: A 'Binding' cannot be used within a 'ArrayList' collection. A 'Binding' can only be set on a DepedencyProperty or a DependencyObject.
I've tried one of the suggested answers e.g. adding the ViewModel as a Dependency Object to my converter but that isn't working
public class TestConverter : DependencyObject , IValueConverter
{
public static readonly DependencyProperty PropertyTypeProperty = DependencyProperty.Register(
"PropertyType", typeof (DerivedRacingViewModel), typeof (TestConverter), new PropertyMetadata(default(DerivedRacingViewModel)));
public DerivedRacingViewModel PropertyType
{
get { return (DerivedRacingViewModel) GetValue(PropertyTypeProperty); }
set { SetValue(PropertyTypeProperty, value); }
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var x = parameter;
return "Test";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
var x = parameter;
var z = parameter;
throw new NotImplementedException();
}
}
Then changing my xaml to:
<converters:TestConverter x:Key="TestConverter" DerivedRacingViewModel="{Binding}" />
That give me compile time errors:
'DerivedRacingViewModel' was not found in type 'TestConverter'.
The reason behind doing this is I want to have 2 or 3 objects available when I'm doing my ConvertBack, e.g. I need the text that is entered into text box, the value that text box is bound to and the view model. This is where I'm having real difficulty. I've seen other people doing it by splitting strings and stuff but I really don't like that.
You should use an ItemsControl like below :
<TextBlock.Text>
<Binding ElementName="MainGrid" Path="DataContext" Converter="{StaticResource TestConverter}">
<Binding.ConverterParameter>
<ItemsControl>
<ItemsControl.Items>
<Label Content="{Binding ElementName=MainGrid, Path=DataContext}"/>
<Label Content="{Binding ElementName=SomeOtherElement, Path=DataContext}"/>
</ItemsControl.Items>
</ItemsControl>
</Binding.ConverterParameter>
</Binding>
</TextBlock.Text>
TestConverter
public class TestConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
ItemsControl ctrl = parameter as ItemsControl;
Label lbl = ctrl.Items[0] as Label;
var c = lbl.Content;
...
}
I'm displaying a popup with the following code:
<Popup PlacementTarget="{Binding ElementName=categoryTagEditorControl}"
Placement="Bottom">
<Popup.IsOpen>
<MultiBinding Mode="OneWay" Converter="{StaticResource BooleanOrConverter}">
<Binding Mode="OneWay" ElementName="categoryTagEditorControl" Path="IsMouseOver"/>
<Binding RelativeSource="{RelativeSource Self}" Path="IsMouseOver" />
</MultiBinding>
</Popup.IsOpen>
<StackPanel>
<TextBox Text="Some Text.."/>
<DatePicker/>
</StackPanel>
</Popup>
Here's the code of BooleanOrConverter:
public class BooleanOrConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
foreach (object booleanValue in values)
{
if (booleanValue is bool == false)
{
throw new ApplicationException("BooleanOrConverter only accepts boolean as datatype");
}
if ((bool)booleanValue == true)
{
return true;
}
}
return false;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
and its placed into PopupTest.InfoPanels.Windows namespace
when I run this, I'm getting following exception:
Cannot find resource named 'BooleanOrConverter'. Resource names are case sensitive.
What should I change for this to work?
It sounds like your Multibinding doesn't know where to look for the converter. Have you defined the converter as a staticresource? You can either specify the converter in the control's resources or in the included ResourceDictionary. Add a reference to the converter's namespace and then define a ResourceKey for it. Something like:
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:MyConverters">
<UserControl.Resources>
<converters:BooleanOrConverter x:Key="BoolOrConverter"/>
</UserControl.Resources>
... // use converter as you were before
</UserControl>
I have two viewmodels (each with thier own models):
AmendmentViewModel
YearViewModel
each have a property:
AmendmentViewModel.TotalAmended
YearViewModel.TotalCommitted
On the view there is a TabControl and each of the viewmodels is the datacontext for a tabcontrol page. Yes i know I could technically use one view model but its a LOT of code and a large view that Has to be in a tabcontrol.
How do i set the binding of a TextBox to the sum of AmendmentViewModel.TotalAmended & YearViewModel.TotalCommitted ?
You can use a MultiBinding together with an IMultiValueConverter. You can find an example here.
Edit:
Here's an example:
<Grid>
<Grid.Resources>
<sys:String x:Key="dataSource1">42</sys:String>
<sys:String x:Key="dataSource2">22</sys:String>
<local:SubtractionConverter x:Key="subtractionConverter"/>
</Grid.Resources>
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource subtractionConverter}">
<Binding Path="." Source="{StaticResource dataSource1}"/>
<Binding Path="." Source="{StaticResource dataSource2}"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
and the converter:
public class SubtractionConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return (double.Parse((string)values[0]) - double.Parse((string)values[1])).ToString();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}