wpf how to switch parent tab from a nested view - c#

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.

Related

Creating an ItemsControl of Icon buttons with DynamicResource

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>

Disabling WPF ComboBox items based on another property

There's this combo box in a WPF application that is populated from an enum.
How would one disable some of its items? Disabling has to be dynamic, based on another property in the ViewModel.
<ComboBox ItemsSource="{utility:EnumMarkupExtension {x:Type types:SomeEnum}}"
SelectedItem="{Binding Path=MySelection, Converter={StaticResource SomeEnumToString}}" />
EnumMarkupExtension is defined thus:
public sealed class EnumMarkupExtension : MarkupExtension
{
public Type Type { get; set; }
public EnumMarkupExtension(Type type) => this.Type = type;
public override object ProvideValue(IServiceProvider serviceProvider)
{
string[] names = Enum.GetNames(Type);
string[] values = new string[names.Length];
for (int i = 0; i < names.Length; i++)
values[i] = Resources.ResourceManager.GetString(names[i]);
return values;
}
}
(SomeEnumToString is an IValueConverter which probably is not relevant to this)
Is there some obvious method of doing this that I'm missing?
I've seen solutions like this
https://www.codeproject.com/Tips/687055/Disabling-ComboboxItem-in-ComboBox-Control-using-C
but can't figure out how to pass a property to IValueConverter since ConverterParameter is not bindable.
I know you have solved the issue with the help of #clemens. Still posting an answer for others who might same question.
Assuming VMDataSource is datasource in Viewmodel and with each item in VMDataSource contains a boolean flag to indicate whether to enable/ disable the item. Following code snippet would work as you queried in the post.
<ComboBox ItemsSource="{Binding VMDataSource}">
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Setter Property="IsEnabled" Value="{Binding Path="VMDisableItem"/>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
Assuming VMDataSource is datasource in Viewmodel and viewmodel contains a Non-boolean property to indicate whether to enable/ disable the item instead of datasource itself containing a bool property to enable/diasble the item(Incase of enums).
In this case a multibinding and a converter that would take the item in VMDataSource and a
non-boolean property to enable/disable an item as follows
<ComboBox DataContext="{StaticResource viewmodelInstance}"
ItemsSource="{Binding VMDataSource}">
<ComboBox.Resources>
<local:Disabler x:Key="disabler"/>
</ComboBox.Resources>
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Setter Property="IsEnabled">
<Setter.Value>
<MultiBinding Converter="{StaticResource disabler}">
<!--Here you can bind the proper u want to use in -->
<!--converter to enable/ disable comboboxitem-->
<!--Currently it is bound property of viewmodelInstance that as non-boolean property-->
<Binding
RelativeSource="{RelativeSource Mode=FindAncestor,AncestorType=ComboBox}"
Path="DataContext.VMDisableItem"/>
<!--Bind to the current comboboxitem which needs to enabled/disabled-->
<Binding />
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
Assuming typeof(VMDisableItem) and typeof(VMDataSource) are both string.
When both are equal, disable the item otherwise enable.
class Disabler : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return !Equals(values[0], values[1]);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
You can use a multiBinding where one of the values is instead of the ConverterParameter. See this answer
Edit the comboboxitem itemtemplate, in the item template bind IsEnabled with the value of the viewmodel (it should be an index) and converter paraemeter the value of the item itself.

Update DataGrid Background from Binding in ViewModel

Today I found something strange (for me). If I want to update a DataGrid through a property in my ViewModel the Binding wont get notified. The special think here is (I think) the Binding is bound to another object (part of an Collection) not directly the property i change.
I have prepared some sample code for you. But first a little (Depper) explanation.
Of course it is just a sample but here I have a ViewModel with two public Properties (Items and CurrentItem). Items is a ObservableCollection and serves as the ItemsSource of my DataGrid. CurrentItem is a String which serves as indicator for a converter to set the background colour (for the grid).
I add two instances o String to my Collection and after the program is started the behaviour is as expected. The first line is green the second is white (set through the converter and the properties).
But if I change the value of CurrentItem after the program was loaded (lets say through the button) the colours wont update on my Datagrid.
If I create a breakpoint at the beginning of the converter I can see (after the loading process) the converter wont execute again so it has to be a Problem with the Binding. I think the problem is my property which is not part of the items in my Collection. The OnPropertyChange method seems to not trigger the update for the RowStyle properly.
In real life the model class of the collections is not a string and the model class implements INotifyPropertyChange (but I don´t think this is the problem cause i just don´t update anything in the model).
I need this kind of behaviour to visible highlight more rows based on a dynamic indicator (similar to the example). If no one knows a better way I think I will implement some kind of Property in my models and update the property with a method from the ViewModel.
ViewModel:
public class MainWindowViewModel : INotifyPropertyChanged
{
public static MainWindowViewModel instance = null;
private string _CurrentItem;
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string Property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(Property));
}
}
public string CurrentItem
{
get
{
return _CurrentItem;
}
set
{
if (value != _CurrentItem)
{
_CurrentItem = value;
OnPropertyChanged("CurrentItem");
OnPropertyChanged("Items");
}
}
}
public ObservableCollection<String> Items { get; set; }
public MainWindowViewModel()
{
instance = this;
Items = new ObservableCollection<string>();
CurrentItem = "First";
Items.Add("First");
Items.Add("Second");
Items.Add("First");
}
View XAML
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Window.Resources>
<local:StringToColorConverter x:Key="StringToColorConverter" />
</Window.Resources>
<DockPanel Margin="30">
<Button DockPanel.Dock="bottom" Content="From First to Second" Click="Button_Click" />
<DataGrid IsReadOnly="True" ItemsSource="{Binding Items}" ColumnWidth="*" AutoGenerateColumns="False">
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="Background"
Value="{Binding Converter={StaticResource StringToColorConverter}}" />
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTextColumn Header="Text" Binding="{Binding}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
Converter
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var item = value as string;
if (item == MainWindowViewModel.instance?.CurrentItem)
return "Green";
return "White";
}
So sorry for the long post I hope you can comprehend my problem and of course maybe help me :)
You can involve CurrentItem using an IMultiValueConverter
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="Background">
<Setter.Value>
<MultiBinding Converter="{StaticResource MultiStringToColorConverter}">
<Binding />
<Binding Path="DataContext.CurrentItem"
RelativeSource="{RelativeSource FindAncestor,
AncestorType={x:Type Window}}"/>
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</DataGrid.RowStyle>
The Converter
public class MultiStringToColorConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
var item = values[0] as string;
var current = values[1] as string;
if (item == current)
return new SolidColorBrush(Colors.Green);
return new SolidColorBrush(Colors.White);
}
public object[] ConvertBack(object values, Type[] targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Everything is normal !
It is by design.
Your background is bound to the n-th object of the DataGrid.
It is not bound to CurrentItem, so there is no reason the binding updates the n-th line background.
Because you have an ObservableCollection, you could put a IsSelected property in MyItem class
And you should make MyItem ràise a PropertyChanged event on IsSelected property.
Of course MyItem would implement INotifyPropertyChanged
Last, you should change the binding :
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="Background"
Value="{Binding Path=IsSelected,
Converter={StaticResource BooleanToColorConverter}}" />
</Style>
</DataGrid.RowStyle>
Of course changing the StringToColorConverter into BooleanToColorConvert wouldbe trivial.
Regards

Inverted bool to radio button from dataContext to userControl

How can I bind an inverted value of a bool in my xaml?
I know, I know, there are a lot of q&a about this, but there aren't any about a specific case: when the converter is in a view model and the radio button in a user control.
how can I correctly refer to the main window view model data context? In other case I used the code I've posted, but in this case I don't get how to use it.
UPDATE CODE:
namespace ***.ViewModel
{
[ValueConversion(typeof(bool), typeof(bool))]
public class InverseBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (targetType != typeof(bool))
throw new InvalidOperationException("The target must be a boolean");
return !(bool)value;
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
class MainWindowViewModel : MyClass.UtilityClasses.ViewModelBase
{
public MainWindowViewModel(){
SaveCommand = new DelegateCommand(SaveData, CanSave );
UndoCommand = new DelegateCommand(UndoActions, CanSave);
[...]
}
....
}
ANOTHER CODE UPDATE:
In my xaml, I have a devexpress GridControl where I list my observableCollection from db. When I click one element, a layout group is populated with the relative data.
In this layout group, I have:
<StackPanel Margin="0" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Left">
<RadioButton Content="{DynamicResource Locale}" Margin="10,0,0,0" x:Name="rd_LOCALE" VerticalAlignment="Center" IsChecked="{Binding Path=REMOTO, Converter={StaticResource InverseBooleanConverter}}" GroupName="Location" Panel.ZIndex="9" TabIndex="10" />
<RadioButton Content="{DynamicResource Remoto}" Margin="10,0,6,0" x:Name="rd_REMOTO" VerticalAlignment="Center" IsChecked="{Binding REMOTO}" GroupName="Location" Panel.ZIndex="10" TabIndex="11" Tag="PRISMA" />
</StackPanel>
WhenI change from one record to another, the converter gives me error... why? It fails the "if (targetType != typeof(bool))" row. So I tried to change that like:
if (value.getType() != typeof(bool))
but in this way, without fails nothing in the code, it put the wrong value in the radiobutton!
You need to make sure that the value converter is referenced in the XAML
<UserControl.Resources>
<myproject:InverseBooleanConverter x:Key="InverseBooleanConverter" />
</UserControl.Resources>
where my project is the namespace in which the value converter class is defined

Binding Radiobuttons to their enumvalues without using a converter

I have a radio button group in a listview. The rows of this listview (which contain the radio button grp amongst other things) is an observable collection.
the code I have written goes something like this:
The Xaml:
<RadioButton Content="EnumValueName1"
GroupName="RadButGrp1"
IsChecked="{Binding propertyName,Mode=TwoWay,Converter={StaticResource EnumToBoolConverter},ConverterParameter=EnumValueName1}" >
</RadioButton>
<RadioButton Content="EnumValueName2"
GroupName="RadButGrp1"
IsChecked="{Binding propertyName,Mode=TwoWay,Converter={StaticResource EnumToBoolConverter},ConverterParameter=EnumValueName2}">
</RadioButton>
<RadioButton Content="EnumValueName3"
GroupName="RadButGrp1"
IsChecked="{Binding propertyName,Mode=TwoWay,Converter={StaticResource EnumToBoolConverter},ConverterParameter=EnumValueName3}">
</RadioButton>
I am trying to bind directly to the data field called propertyName in my data structure defining the table that holds these values. I do NOT have this field in my ViewModel class for this view. I did this to avoid keeping track of the index of the collection that I am currently populating. (or so i'd like to think!)
The converter:
public class EnumBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string parameterString = parameter as string;
if (parameterString == null)
return DependencyProperty.UnsetValue;
if (value == null || Enum.IsDefined(value.GetType(), value) == false)
return DependencyProperty.UnsetValue;
object parameterValue = Enum.Parse(value.GetType(), parameterString);
return parameterValue.Equals(value);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string parameterString = parameter as string;
if (parameterString == null || value.Equals(false))
return DependencyProperty.UnsetValue;
return Enum.Parse(targetType, parameterString);
}
}
The problem is that in the ConvertBack function at the Enum.Parse line, the following Argument exception occurs:
Type provided must be an Enum.
Parameter name: enumType
Is there a way to return an enum type to the binding? How do I tell the radio buttons which enumeration value it represents? How do I write a function that returns the appropriate enum value to the binding?
Hoping you guys can help. Thanks in advance!
Try this, it's my version of the EnumToBoolConverter:
public class EnumToBoolConverter : BaseConverterMarkupExtension<object, bool>
{
public override bool Convert(object value, Type targetType, object parameter)
{
if (value == null)
return false;
return value.Equals(Enum.Parse(value.GetType(), (string)parameter, true));
}
public override object ConvertBack(bool value, Type targetType, object parameter)
{
return value.Equals(false) ? DependencyProperty.UnsetValue : parameter;
}
}
Ok the solution was relatively simple once I got the concept right. I have done the following which partially solves my problem.
<RadioButton Content="EnumValueName1"
GroupName="RadBtnGrp1"
IsChecked="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ParentControl}},
Path=DataContext.propName,
Mode=TwoWay,
Converter={StaticResource EnumToBoolConverter},ConverterParameter=EnumValueName1}">
</RadioButton>
The targetType in my ConvertBack function now the correct enum type. Hope this helps!
Now i have to figure out how to make the radiobuttons retain selections in multiple rows of the listview. Presently they a selection in first row deselects the same group from the rest of the rows.
Thank you for your help so far. If anyone can point me to a solution for the new problem that would be really great!
Recommend you to create the radio buttons dynamically, ListBox can help us do that, without converters. The advantage of this method is below:
if someday your enum class changes, you do not need to update the GUI (XAML file).
The steps of this method are below:
create a ListBox and set the ItemsSource for the listbox as the enum and binding the SelectedItem of the ListBox to the selected property.
Then the Radio Buttons for each ListBoxItem will be created.
Step 1: Re-define your Enum.
public enum EnumValueNames
{
EnumValueName1,
EnumValueName2,
EnumValueName3
}
Then add below property to your DataContext (or ViewModel of MVVM), which records the selected item which is checked.
public EnumValueNames SelectedEnumValueName { get; set; }
Step 2: add the enum to static resources for your Window, UserControl or Grid etc.
<Window.Resources>
<ObjectDataProvider MethodName="GetValues"
ObjectType="{x:Type system:Enum}"
x:Key="EnumValueNames">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="local:EnumValueNames" />
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</Window.Resources>
Step 3: Use the List Box and Control Template to populate each item inside as Radio button
<ListBox ItemsSource="{Binding Source={StaticResource EnumValueNames}}" SelectedItem="{Binding SelectedEnumValueName, Mode=TwoWay}" >
<ListBox.Resources>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<RadioButton
Content="{TemplateBinding ContentPresenter.Content}"
IsChecked="{Binding Path=IsSelected,
RelativeSource={RelativeSource TemplatedParent},
Mode=TwoWay}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.Resources>
</ListBox>
References:
https://www.codeproject.com/Articles/130137/Binding-TextBlock-ListBox-RadioButtons-to-Enums

Categories