So I have a combobox I'd like to reuse for multiple sets of data rather than having 3 separate comboboxes. Maybe this is bad and someone can tell me so. I'm open to all ideas and suggestions. I'm just trying to clean up some code and thought one combobox rather than 3 was cleaner. Anyway the ItemsSource
and SelectedItem
all should change when another ComboBox's
value is changed which Raises the Property Changed value for the ComboBox that isn't working. The worst part is when CurSetpoint.ActLowerModeIsTimerCondition
is true it always loads the SelectedItem
correctly but when going from that to CurSetpoint.ActLowerGseMode
being True the combobox doesn't have the SelectedItem
loaded.
Here is the XAML for the ComboBox with issues.
<ComboBox Grid.Row="1" Grid.Column="1" Margin="5,2" VerticalAlignment="Center" Name="cmbActTimersSetpointsGseVars">
<ComboBox.Style>
<Style BasedOn="{StaticResource {x:Type ComboBox}}" TargetType="{x:Type ComboBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=CurSetpoint.ActLowerModeIsTimerCondition}" Value="True">
<Setter Property="ItemsSource" Value="{Binding TimerInstances}" />
<Setter Property="SelectedItem" Value="{Binding CurSetpoint.ActLowerTimerInstance, Mode=TwoWay}" />
<Setter Property="DisplayMemberPath" Value="DisplayName"></Setter>
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=CurSetpoint.ActLowerGseMode}" Value="True">
<Setter Property="ItemsSource" Value="{Binding EnabledGseVars}" />
<Setter Property="SelectedItem" Value="{Binding CurSetpoint.ActLowerGseVar, Mode=TwoWay}" />
<Setter Property="DisplayMemberPath" Value="DisplayName"></Setter>
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=CurSetpoint.ActModeIsLogicCondition}" Value="True">
<Setter Property="ItemsSource" Value="{Binding SetpointStates}" />
<Setter Property="SelectedItem" Value="{Binding CurSetpoint.ActSetpoint1State, Mode=TwoWay}" />
<Setter Property="DisplayMemberPath" Value="Value"></Setter>
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=CurSetpoint.ShowActLowerCmbBox}" Value="False">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
Here is an image of the two combo boxes. When the Mode is change from Timer to Variable it doesn't load anything despite its binding not being null and its instance and itemssource instance data not having changed. But If I go from Variable to Timer the Timer: 1 is displayed correctly.
Here is the Model Code behind the Mode ComboBox's value being changed. Along with the other two Properties that are the SelectedItems
for the ComboBox in question. Along with the Properties of the ItemsSource
private VarItem actLowerMode;
public VarItem ActLowerMode
{
get { return this.actLowerMode; }
set
{
if (value != null)
{
var oldValue = this.actLowerMode;
this.actLowerMode = value;
config.ActLowerMode.Value = value.ID;
//if they weren't the same we need to reset the variable name
//Note: 5/21/19 Needs to be this way instead of before because when changing from Timer->GseVariable it wouldn't change the config value because it
//thought it was still a timer condition because the value hadn't been changed yet.
if (oldValue != null && (oldValue.CheckAttribute("timer") != value.CheckAttribute("timer")))
{
if (value.CheckAttribute("timer"))
{
ActLowerTimerInstance = model.TimerInstances.First();
}
else
{
ActLowerVarName = "";
if (GseMode)
{
ActLowerGseVar = model.EnabledGseVars.FirstOrDefault();
}
}
}
RaisePropertyChanged("ActLowerMode");
RaisePropertyChanged("HasActLowerScale");
RaisePropertyChanged("ActLowerGseMode");
RaisePropertyChanged("HasActLowerVarName");
RaisePropertyChanged("ActLowerModeIsConstant");
RaisePropertyChanged("ActLowerRow1Label");
RaisePropertyChanged("ActLowerModeIsTimerCondition");
RaisePropertyChanged("ShowActLowerConstTextBox");
RaisePropertyChanged("ShowActLowerCmbBox");
RaisePropertyChanged("ShowActLowerRow1Label");
if (GseMode)
{
RaisePropertyChanged("ActLowerGseMode");
}
}
}
}
private GseVariableModel actLowerGseVar;
public GseVariableModel ActLowerGseVar
{
get { return this.actLowerGseVar; }
set
{
if (value != null)
{
this.actLowerGseVar = value;
if (!ActLowerModeIsTimerCondition)//only changing the config value if its not set to timer
{
config.ActLowerVarName.Value = value.Number.ToString();
}
RaisePropertyChanged("ActLowerGseVar");
}
}
}
private INumberedSelection actLowerTimerInstance;
public INumberedSelection ActLowerTimerInstance
{
get { return this.actLowerTimerInstance; }
set
{
if (value != null)
{
this.actLowerTimerInstance = value;
config.ActLowerVarName.Value = value.Number.ToString();
RaisePropertyChanged("ActLowerTimerInstance");
}
}
}
public ObservableCollection<INumberedSelection> TimerInstances { get { return this.timerInstances; } }
public ObservableCollection<GseVariableModel> EnabledGseVars
{
get
{
return enabledGseVariables;
}
}
I'm sure I've probably overlooked some important info so I will update it with any questions you guys have or details you need.
Update: Just wanted to add as stated in the bounty. That if what I'm doing here isn't a good idea and there is a better way of doing it, someone with experience please just tell me why and how I should. If there are better ways and what I'm doing is bad I just need to know.
There's nothing wrong with binding multiple ComboBox
es and setting their Visibility
. For one, it greatly reduces the complexity compared to the code from your post.
Nonetheless, you can easily swap the context (not to be confused with DataContext
) of an ItemsControl
by introducing an additional abstraction between the viewmodel and the view.
This is how it works:
ItemsControl
Your idea of gathering the properties per entity is certainly a good one. Though the implementation could be better, both the viewmodel and the view look bloated. That's what this context object is all about, gathering and keeping state as you swap contexts back and forth.
Starting with our model classes. Let's code against an interface (even though ItemsSource is untyped).
namespace WpfApp.Models
{
public interface IEntity
{
string Name { get; }
}
public class Dog : IEntity
{
public Dog(string breed, string name)
{
Breed = breed;
Name = name;
}
public string Breed { get; }
public string Name { get; }
}
public class Author : IEntity
{
public Author(string genre, string name)
{
Genre = genre;
Name = name;
}
public string Genre { get; }
public string Name { get; }
}
}
Next, the ViewModels, starting with our context.
namespace WpfApp.ViewModels
{
public class ItemsContext : ViewModelBase
{
public ItemsContext(IEnumerable<IEntity> items)
{
if (items == null || !items.Any()) throw new ArgumentException(nameof(Items));
Items = new ObservableCollection<IEntity>(items);
SelectedItem = Items.First();
}
public ObservableCollection<IEntity> Items { get; }
private IEntity selectedItem;
public IEntity SelectedItem
{
get { return selectedItem; }
set
{
selectedItem = value;
OnPropertyChanged();
}
}
public string DisplayMemberPath { get; set; }
}
}
As said, the relevant properties, with notifications for SelectedItem
, nothing special. We immediately see the effect on our MainViewModel
.
namespace WpfApp.ViewModels
{
public class MainViewModel : ViewModelBase
{
private readonly ItemsContext _dogContext;
private readonly ItemsContext _authorContext;
public MainViewModel()
{
_dogContext = new ItemsContext(FetchDogs()) { DisplayMemberPath = nameof(Dog.Breed) };
_authorContext = new ItemsContext(FetchAuthors()) { DisplayMemberPath = nameof(Author.Genre) };
}
private ItemsContext selectedContext;
public ItemsContext SelectedContext
{
get { return selectedContext; }
set
{
selectedContext = value;
OnPropertyChanged();
}
}
private bool dogChecked;
public bool DogChecked
{
get { return dogChecked; }
set
{
dogChecked = value;
if(dogChecked) SelectedContext = _dogContext;
}
}
private bool authorChecked;
public bool AuthorChecked
{
get { return authorChecked; }
set
{
authorChecked = value;
if(authorChecked) SelectedContext = _authorContext;
}
}
private static IEnumerable<IEntity> FetchDogs() =>
new List<IEntity>
{
new Dog("Terrier", "Ralph"),
new Dog("Beagle", "Eddy"),
new Dog("Poodle", "Fifi")
};
private static IEnumerable<IEntity> FetchAuthors() =>
new List<IEntity>
{
new Author("SciFi", "Bradbury"),
new Author("RomCom", "James")
};
}
}
Two cleanly separated flows, each managing its own context. It's clear you could easily extend this to any number of contexts, without them getting in each others way. Now, to apply the context to our ItemsControl
we have two options. We could subclass our Control
or use an Attached Property. Favoring composition over inheritance, here's the class with the AP.
namespace WpfApp.Extensions
{
public class Selector
{
public static ItemsContext GetContext(DependencyObject obj) => (ItemsContext)obj.GetValue(ContextProperty);
public static void SetContext(DependencyObject obj, ItemsContext value) => obj.SetValue(ContextProperty, value);
public static readonly DependencyProperty ContextProperty =
DependencyProperty.RegisterAttached("Context", typeof(ItemsContext), typeof(Selector), new PropertyMetadata(null, OnItemsContextChanged));
private static void OnItemsContextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var selector = (System.Windows.Controls.Primitives.Selector)d;
var ctx = (ItemsContext)e.NewValue;
if (e.OldValue != null) // Clean up bindings from previous context, if any
{
BindingOperations.ClearBinding(selector, System.Windows.Controls.Primitives.Selector.SelectedItemProperty);
BindingOperations.ClearBinding(selector, ItemsControl.ItemsSourceProperty);
BindingOperations.ClearBinding(selector, ItemsControl.DisplayMemberPathProperty);
}
selector.SetBinding(System.Windows.Controls.Primitives.Selector.SelectedItemProperty, new Binding(nameof(ItemsContext.SelectedItem)) { Source = ctx, Mode = BindingMode.TwoWay });
selector.SetBinding(ItemsControl.ItemsSourceProperty, new Binding(nameof(ItemsContext.Items)) { Source = ctx });
selector.SetBinding(ItemsControl.DisplayMemberPathProperty, new Binding(nameof(ItemsContext.DisplayMemberPath)) { Source = ctx });
}
}
}
That covers both steps 2 and 3. You can tweak this however you like. For example, we've made ItemsContext.DisplayMemberPath
a non notifying prop, so you could just set the value directly instead of through binding.
Finally, the view, where it all comes together.
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:WpfApp.ViewModels"
xmlns:ext="clr-namespace:WpfApp.Extensions"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800" WindowStartupLocation="CenterScreen">
<Window.DataContext>
<vm:MainViewModel/>
</Window.DataContext>
<Window.Resources>
<Style x:Key="SelectorStyle" TargetType="{x:Type Selector}">
<Setter Property="Width" Value="150"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="Margin" Value="0,20"/>
</Style>
</Window.Resources>
<StackPanel Margin="20">
<RadioButton GroupName="Entities" Content="Dogs" IsChecked="{Binding DogChecked}" />
<RadioButton GroupName="Entities" Content="Authors" IsChecked="{Binding AuthorChecked}" />
<ComboBox ext:Selector.Context="{Binding SelectedContext}" Style="{StaticResource SelectorStyle}" />
<ListBox ext:Selector.Context="{Binding SelectedContext}" Style="{StaticResource SelectorStyle}" />
<DataGrid ext:Selector.Context="{Binding SelectedContext}" Style="{StaticResource SelectorStyle}" />
</StackPanel>
</Window>
The cool thing about the Attached Property is that we're coding against the abstract Selector
control, which is a direct descendant of ItemsControl
. So without changing our lower layers we can share our context with ListBox
and DataGrid
as well.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With