WPF: Bind TabControl SelectedIndex to a View Model's Enum Property - c#

I have a property on my ViewModel that is an enum:
ViewModel:
public MyViewModel {
// Assume this is a DependancyProperty
public AvailableTabs SelectedTab { get; set; }
// Other bound properties
public string Property1 { get; set; }
public string Property2 { get; set; }
public string Property3 { get; set; }
}
public enum AvailableTabs {
Tab1,
Tab2,
Tab3
}
I'd like to be able to bind SelectedIndex (or SelectedItem) of my TabControl to this property and have it correctly set the appropriate tab using a converter. Unfortunately, I'm a bit stuck. I know I can easily just use the SelectedIndex in my model, but I want the flexibility of re-ordering the tabs without breaking anything. I've given each TabItem a Tag property of the applicable enum value.
My XAML:
<TabControl Name="MyTabControl" SelectedIndex="{Binding SelectedTab, Converter={StaticResource SomeConverter}}">
<TabItem Header="Tab 1" Tag="{x:Static local:AvailableTabs.Tab1}">
<TextBlock Text="{Binding Property1}" />
</TabItem>
<TabItem Header="Tab 2" Tag="{x:Static local:AvailableTabs.Tab2}">
<TextBlock Text="{Binding Property2}" />
</TabItem>
<TabItem Header="Tab 3" Tag="{x:Static local:AvailableTabs.Tab3}">
<TextBlock Text="{Binding Property3}" />
</TabItem>
</TabControl>
My problem is that I can't figure out how to get the TabControl into my converter so I can do:
// Set the SelectedIndex via the enum (Convert)
var selectedIndex = MyTabControl.Items.IndexOf(MyTabControl.Items.OfType<TabItem>().Single(t => (AvailableTabs) t.Tag == enumValue));
// Get the enum from the SelectedIndex (ConvertBack)
var enumValue = (AvailableTabs)((TabItem)MyTabControl.Items[selectedIndex]).Tag;
I'm afraid I might be overthinking it. I tried using a MultiValue converter without much luck. Any ideas?

You simply need a converter that casts the value to an index.
public class TabConverter : 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 (AvailableTabs)value;
}
}

Instead of specifying the values in the XAML, I would bind ItemsSource to an array of values from your enum:
Code:
public AvailableTabs[] AvailableTabs => Enum.GetValues(typeof(AvailableTabs Enum)).Cast<AvailableTabs>().ToArray();
XAML:
<TabControl Name="MyTabControl" SelectedIndex="{Binding SelectedTab}" ItemsSource="{Binding AvailableTabs}" />

The important thing is that the solution should work if I change the tab order in the TabControl
if this is not important then the elements in the enum type
and the views in the TabControl have to be in the same order
the conversion in this case is to cast the enum value to int value
I name the views in TabControl according to enum values
AppTab.ValidDates (enum value) corresponds to validDatesView (view name)
enum values
public enum AppTab
{
Parameters, ValidDates, ...
}
views in TabControl
<TabControl Name="myTabControl" SelectedIndex="{Binding SelectedTab, Converter={c:AppTabToIntConverter}}"
IsEnabled="{Binding IsGuiEnabled}">
<TabItem Header="{x:Static r:Resource.Parameters}">
<view:ParametersView x:Name="parametersView"/>
</TabItem>
<TabItem Header="{x:Static r:Resource.Dates}">
<view:ValidDatesView x:Name="validDatesView"/>
</TabItem>
fill up ViewNameIndexDictionary and IndexViewNameDictionary
Window_Loaded event is too late, AppTabToIntConverter runs before that
public static Dictionary<AppTab, int> ViewNameIndexDictionary { get; set; } = new Dictionary<AppTab, int>();
public static Dictionary<int, AppTab> IndexViewNameDictionary { get; set; } = new Dictionary<int, AppTab>()
private void Window_Initialized(object sender, EventArgs e)
{
var i = 0;
foreach (TabItem item in myTabControl.Items)
{
var tabContentName = ((FrameworkElement)item.Content).Name;
// Convert TabItem name "validDatesView" to "ValidDates"
var appTabString = tabContentName.FirstCharToUpper().CutLastNCharacter("View".Length);
var appTab = (AppTab)Enum.Parse(typeof(AppTab), appTabString);
ViewNameIndexDictionary.Add(appTab, i);
IndexViewNameDictionary.Add(i, appTab);
i++;
}
}
the converter
// The root tag must contain: xmlns:c="clr-namespace:LedgerCommander.ValueConverter"
class AppTabToIntConverter : BaseValueConverter<AppTabToIntConverter>
{
public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (!(value is AppTab appTab))
throw new Exception("The type of value is not AppTab");
return MainWindowView.ViewNameIndexDictionary[appTab];
}
public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (!(value is int tabIndex))
throw new Exception("The type of value is not int");
return MainWindowView.IndexViewNameDictionary[tabIndex];
}
}
the BaseValueConverter (thanks to AngelSix)
public abstract class BaseValueConverter<T> : MarkupExtension, IValueConverter where T : class, new()
{
private static T Converter = null;
public override object ProvideValue(IServiceProvider serviceProvider)
{
return Converter ?? (Converter = new T());
}
public abstract object Convert(object value, Type targetType, object parameter, CultureInfo culture);
public abstract object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture);
}

Related

`IsChecked` Binding Fails for no Apparent Reason

I’m trying to learn XAML (without much success) – be gentle.
I have a toggle button, a slider and a button. This is what I am trying to do:
When the slider’s value is changed the toggle button should turn off – works fine.
When the button is pressed, set the slider value and keep the toggle button in whichever state it is – does not work; after I push the button, (1) does not work anymore. It behaves like the IsChecked binding was cleared.
Ideally I would like to write everything in XAML, but I guess it is not possible.
// MainWindow.xaml.cs
public class ValueToFalse : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
var saved = toggle.IsChecked;
slider.Value = 2;
toggle.IsChecked = saved;
}
}
<!--App.xaml-->
<local:ValueToFalse x:Key="ValueToFalse"/>
<!--MainWindow.xaml-->
<ToggleButton
Name="toggle"
Content="toggle"
HorizontalAlignment="Left"
Margin="479,64,0,0"
VerticalAlignment="Top"
Height="108"
Width="192"
IsChecked="{Binding Value, Mode=OneWay, ElementName=slider, Converter={StaticResource ValueToFalse} }"
/>
<Slider
Name="slider"
HorizontalAlignment="Left"
Margin="167,105,0,0"
VerticalAlignment="Top"
Width="233"
Height="35"
/>
<Button
Content="Button"
HorizontalAlignment="Left"
Margin="346,340,0,0"
VerticalAlignment="Top"
Click="Button_Click"
/>
I didn't quite understand what you want to implement.
But nevertheless, you have an obvious mistake that #Clemens pointed out - if you need to assign a Dependency Property value and keep the binding from it, then the binding must be in the direction of the TwoWay or OneWaySource source.
Since you do not need to assign a value to the source, but you need to store the direction to the source in the binding, you can solve this by implementing a converter.
I show the full implementation of the converter with markup extension. This may be useful to you in the future.
[ValueConversion(typeof(object), typeof(bool))]
public class ValueToFalseConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
// This value tells the binding that the result of the conversion should not be assigned.
return Binding.DoNothing;
}
private ValueToFalseConverter() { }
public static ValueToFalseConverter Instance { get; } = new ValueToFalseConverter();
}
[MarkupExtensionReturnType(typeof(ValueToFalseConverter))]
public class ValueToFalseExtension : MarkupExtension
{
public override object ProvideValue(IServiceProvider serviceProvider)
{
return ValueToFalseConverter.Instance ;
}
}
<ToggleButton
----------
----------
IsChecked="{Binding Value,
Mode=TwoWay,
ElementName=slider,
Converter={local:ValueToFalse}}"
/>

Xamarin Forms Xaml Converter: Bindable property

In Xamarin Forms I'm trying to create a xaml converter with properties.
This is to be used, for example, to show values from a list in different ways, based on a code behind property.
I based my code on this: https://stackoverflow.com/a/29869734.
Converter:
namespace App2.Converters
{
class MyConverter : IValueConverter
{
public int ConvParam { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return $"value: {value} - ConvParam: {ConvParam}";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
XAML:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:conv="clr-namespace:App2.Converters"
x:Class="App2.MainPage"
x:Name="MainPageXaml">
<ContentPage.Resources>
<conv:MyConverter x:Key="cnv" ConvParam="{Binding Source={Reference MainPageXaml}, Path=PropParam}" />
<!--<conv:MyConverter x:Key="cnv" ConvParam="333" />-->
</ContentPage.Resources>
<StackLayout Orientation="Vertical">
<!-- Place new controls here -->
<Label Text="{Binding Source={Reference MainPageXaml}, Path=PropVal}" />
<Label Text="{Binding Source={Reference MainPageXaml}, Path=PropParam}" />
<Label Text="{Binding Source={Reference MainPageXaml}, Path=PropVal, Converter={StaticResource cnv}}" />
</StackLayout>
Code behind:
public partial class MainPage : ContentPage
{
public int PropVal { get; set; } = 111;
public int PropParam { get; set; } = 222;
public MainPage()
{
InitializeComponent();
}
}
The goal is to bind ConvParam of my converter to PropParam in code behind.
But if I use:
<conv:MyConverter x:Key="cnv" ConvParam="{Binding Source={Reference MainPageXaml}, Path=PropParam}" />
the error Position 10:39. No property, bindable property, or event found for 'ConvParam', or mismatching type between value and property is shown and the app doesn't compile.
The property ConvParam itself is recognized inside xaml: if I replace the above line with
<conv:MyConverter x:Key="cnv" ConvParam="333" />
everything works.
The binding expression I used ({Binding Source={Reference MainPageXaml}, Path=PropParam}) actually works, if used as source for the text property of a label:
<Label Text="{Binding Source={Reference MainPageXaml}, Path=PropParam}" />
But if I use it in Resources, It doesn't work.
Thanks to Julipan I could make it work!
As he pointed out, ConvParam must be a BindableProperty, so I modified my converter to inherit from BindableObject and defined ConvParam as BindableProperty.
Converter:
namespace App2.Converters
{
class MyConverter : BindableObject, IValueConverter
{
public static readonly BindableProperty ConvParamProperty = BindableProperty.Create(nameof(ConvParam), typeof(int), typeof(MyConverter));
public int ConvParam
{
get { return (int)GetValue(ConvParamProperty); }
set { SetValue(ConvParamProperty, value); }
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return $"value: {value} - ConvParam: {ConvParam}";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

Binding radio button to enum property

I think I've followed the examples given in this post but my property is not changing when button are changed. Any suggestions on where I went wrong?
C# code for enum and class
public enum SystemTypes
{
TypeA,
TypeB
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
SystemTypes systemType = SystemTypes.TypeA;
public SystemTypes SystemType
{
get { return systemType; }
set { systemType = value; }
}
}
public class EnumToBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value.Equals(parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value.Equals(true) ? parameter : Binding.DoNothing;
}
}
xaml
<Canvas>
<Canvas.Resources>
<local:EnumToBooleanConverter x:Key="EnumToBooleanConverter" />
</Canvas.Resources>
<RadioButton x:Name="TypeARadioButton" Content="TypeA" Canvas.Left="10" Canvas.Top="10"
IsChecked="{Binding Path=SystemType, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter={x:Static local:SystemTypes.TypeA}}" />
<RadioButton x:Name="TypeBRadioButton" Content="TypeB" Canvas.Left="10" Canvas.Top="31"
IsChecked="{Binding Path=SystemType, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter={x:Static local:SystemTypes.TypeB}}" />
</Canvas>
You need to set Binding Mode to TwoWay, then in Converter implement method ConvertBack responsible for converting bool to SystemTypes, in settter of SystemType include
set { systemType = value; OnPropertyChanged(() => "SystemType");}
in order to fill property in that its value was changed.
OnPropertyChanged(() => "SystemType")
can work if you implement interface INotifyPropertyChanged. I cannot you whether you set DataContext, if you did not binding is not working. In order to rectify this after InitializeComponent() add
this.DataContext = this;

Philosophy on Binding to Parameters in WPF

So I'm trying to build out a project that will allow a user to type some text into a textbox on the left side of the form and that will filter out the available items from my datasource list.
<Label Content="Enter item name below"></Label>
<TextBox Name="SearchTermTextBox" TabIndex="0" Text="" />
I was under the impression I could bind to the datasource the list then use a converter to filter out the items that were unlike the string.
<ListBox DataContext="{Binding Colors}">
<ListBox.ItemsSource>
<MultiBinding Converter="{StaticResource FilterTextValueConverter}" ConverterParameter="{Binding ElementName=SearchTermTextBox, Path=Text}" />
</ListBox.ItemsSource>
<ListBox.ItemTemplate>
//etc...
</ListBox.ItemTemplate>
</ListBox>
However, you can't bind to an elementname in the converterparameter unless you use something called a dependency property.
Edit: Seeing as I've created confusion with the code above, here's the converter I'm trying to bind:
public class FilterTextValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var trackedColors = value as List<Colors>;
if (trackedColors != null)
return (trackedColors).Where(item => item.ColorName.Contains(parameter.ToString())).ToList();
return null;
}
public object ConvertBack(object value, Type targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class Colors
{
public String ColorName;
public String Description;
}
What is wrong with my approach here? Clearly I'm angering the WPF gods since this is a fairly straightforward operation but I'm being denied it on principle. Any help would be appreciated.
Simple binding with converter will work here, no need for MultiBinding.
<ListBox ItemsSource="{Binding Path=Text, ElementName=SearchTermTextBox,
Converter="{StaticResource FilterTextValueConverter}">
......
</ListBox>
Assuming FilterTextValueConverter is implementing IValueConverter, you can access text from value passed to Convert method.
public class FilterTextValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
string text = value.ToString(); // TEXT for textBox can be accessed here.
return new List<string>(); // Return filtered list from here.
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
return Binding.DoNothing;
}
}
UPDATE
In case you want to pass multiple bindings to converter, use IMultiValueConverter because ConverterParameter is not Dependency property, hence cannot be bound.
XAML
<ListBox DataContext="{Binding Colors}">
<ListBox.ItemsSource>
<MultiBinding Converter="{StaticResource FilterTextValueConverter}">
<Binding/>
<Binding ElementName="SearchTermTextBox" Path="Text"/>
</MultiBinding>
</ListBox.ItemsSource>
</ListBox>
Converter
public class FilterTextValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
var trackedColors = values[0] as List<Colors>;
if (trackedColors != null && !String.IsNullOrEmpty(values[1].ToString()))
return (trackedColors).Where(item =>
item.ColorName.Contains(values[1].ToString())).ToList();
return null;
}
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
I continued looking into this issue well after the accepted answer was posted and working for me. What I discovered is that it's a fairly trivial task to wrap the control you're trying to get a new dependencyproperty out of to allow for proper binding.
I will not be accepting my own answer to this determined so much later, but this seems (in my amateur opinion) like a much more elegant solution than adding a converter despite being a bit more complex:
Note that this is for a new dependency on the caretindex property of a textbox, not for the original question on binding, but it just requires some smart renaming to get it working ;).
public class TextBoxDependencyWrapper : TextBox
{
public static readonly DependencyProperty CaretIndexProperty = DependencyProperty.Register(
"CaretIndex", typeof (int), typeof (TextBoxDependencyWrapper), new FrameworkPropertyMetadata(default(int), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, CaretIndexChanged ));
protected override void OnKeyUp(KeyEventArgs e) //Event that changes the property we're trying to track
{
base.OnKeyUp(e);
CaretIndex = base.CaretIndex;
}
protected override void OnKeyDown(KeyEventArgs e) //Event that changes the property we're trying to track
{
base.OnKeyDown(e);
CaretIndex = base.CaretIndex;
}
public new int CaretIndex
{
get { return (int) GetValue(CaretIndexProperty); }
set { SetValue(CaretIndexProperty, value); }
}
}

How to invoke String.Joing on an ObservableCollection's bound property (to a ListView) that is a List<string>

I have a simple Observable collection with two public properties, int ID and List Targets. The code-behind looks like (simplified code to remove unnecessary and not relevant code):
public class MyClass
{
public ObservableCollection<SomeClass> jobs;
public class SomeClass
{
private int id;
private List<string> targets;
public int ID
{
get { return id; }
set { id = value; }
}
public List<string> Targets
{
get { return targets; }
set { targets = value; }
}
public SomeClass(int _id, List<string> _targets)
{
id = _id;
targets = _targets;
}
}
public MyClass()
{
InitializeComponent();
jobs = new ObservableCollection<SomeClass>();
myListView.ItemsSource = jobs; //jobs is populated from a a loader in Window_Loaded
}
}
The ListView and binding in the xaml look like:
<ListView Name="MyListView" ItemsSource="{Binding Path=jobs, RelativeSource={RelativeSource AncestorType=Window},
Mode=OneWay}" Width="480" Height="155" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,35,10,0" >
<ListView.ContextMenu>
<ContextMenu Name="contextMenuJobRemove">
<ContextMenu.BitmapEffect>
<OuterGlowBitmapEffect />
</ContextMenu.BitmapEffect>
<MenuItem Header="Remove" Click="contextMenuJobRemove_Click" CommandParameter="{Binding RelativeSource={RelativeSource Self}, Path=Parent}"/>
</ContextMenu>
</ListView.ContextMenu>
<ListView.View>
<GridView AllowsColumnReorder="True" ColumnHeaderToolTip="Broadcast call targets">
<GridViewColumn DisplayMemberBinding="{Binding Path=ID}" Header="ID" Width="50" />
<GridViewColumn DisplayMemberBinding="{Binding Path=Targets}" Header="Targets" Width="100" />
</GridView>
</ListView.View>
</ListView>
So when the ListView displays, the Targets columns rightfully displays "(Collection)". Ideally I'd like this column to display something like String.Join(",", Targets.ToArray()). How can this be done, and am I doing this in the xaml or code-behind?
One method would be a converter in the binding.
However this will only update once the property is changed (not the collection contents). So it might be better to expose a display string property in your object and fire change notifications for that property whenever the list changes. If the collection does not change in the first place those notifications are obviously not necessary.
Use a value converter:
[ValueConversion(typeof(IEnumerable<string>), typeof(string))]
public class MyArrayToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
return String.Join (",", ( (IEnumerable<string>)value ).ToArray() );
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotImplementedException ();
}
}
<Window
...
xmlns:my="clr-namespace:XXXXXXXXXXXXXXXXXXXXXCSharpNamespace"
...>
<Window.Resources>
<my:MyArrayToStringConverter x:Key="myconverter" />
</Window.Resources>
...
"{Binding ... Converter={StaticResource myconverter}}"
</Window>
You could create a simple value converter:
public class ListConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
var enumerable = value as IEnumerable;
if (enumerable == null)
return string.Empty;
return String.Join(", ", enumerable.ToArray());
}
public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture)
{
throw new NotImplementedException();
}
}

Categories