Reputation: 1823
I am creating a custom control in WPF that contains a text box, image button and a combo box. I am able to get everything to layout correctly and all the bindings work with everything except for the SelectedItem of the combo box.
Here is the custom control code:
public class GelPakPickerOverlay : Border
{
public static readonly DependencyProperty SelectedGelPakProperty =
DependencyProperty.Register(
"SelectedGelPak",
typeof(object),
typeof(GelPakPickerOverlay),
new FrameworkPropertyMetadata(
null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static readonly DependencyProperty LocationProperty =
DependencyProperty.Register(
"Location",
typeof(string),
typeof(GelPakPickerOverlay),
new FrameworkPropertyMetadata(string.Empty, OnLocationChanged));
public static readonly DependencyProperty GelPakSourceProperty =
DependencyProperty.Register(
"GelPakSource",
typeof(IEnumerable),
typeof(GelPakPickerOverlay),
new FrameworkPropertyMetadata(null, OnGelPakSourceChanged));
public static readonly DependencyProperty SaveCommandProperty =
DependencyProperty.Register(
"SaveCommand",
typeof(ICommand),
typeof(GelPakPickerOverlay),
new FrameworkPropertyMetadata(null, OnSaveCommandChanged));
private static ComboBox gpSelector;
private static TextBox gpLocation;
private static Button saveButton;
public GelPakPickerOverlay()
{
Height = 98;
gpSelector = new ComboBox();
gpSelector.Width = 100;
gpSelector.Margin = new Thickness(10);
gpSelector.HorizontalAlignment = HorizontalAlignment.Left;
gpSelector.VerticalAlignment = VerticalAlignment.Center;
Grid grid = new Grid();
grid.ColumnDefinitions.Add(new ColumnDefinition());
ColumnDefinition def = new ColumnDefinition();
def.Width = new GridLength(40);
grid.ColumnDefinitions.Add(def);
gpLocation = new TextBox();
gpLocation.Style = (Style) FindResource("TextBoxStyleBase");
gpLocation.Width = 70;
gpLocation.Margin = new Thickness(10);
gpLocation.HorizontalAlignment = HorizontalAlignment.Left;
gpLocation.VerticalAlignment = VerticalAlignment.Center;
Grid.SetColumn(gpLocation, 0);
saveButton = new Button();
saveButton.Style = (Style) FindResource("SaveButton");
saveButton.Margin = new Thickness(0, 10, 10, 10);
saveButton.HorizontalAlignment = HorizontalAlignment.Center;
Grid.SetColumn(saveButton, 1);
grid.Children.Add(gpLocation);
grid.Children.Add(saveButton);
StackPanel mainChild = new StackPanel();
mainChild.Orientation = Orientation.Vertical;
mainChild.Children.Add(gpSelector);
mainChild.Children.Add(grid);
Child = mainChild;
}
public object SelectedGelPak
{
get { return GetValue(SelectedGelPakProperty); }
set { SetValue(SelectedGelPakProperty, value); }
}
public string Location
{
get { return GetValue(LocationProperty).ToString(); }
set { SetValue(LocationProperty, value); }
}
public IEnumerable GelPakSource
{
get { return (IEnumerable) GetValue(GelPakSourceProperty); }
set { SetValue(GelPakSourceProperty, value); }
}
public ICommand SaveCommand
{
get { return (ICommand) GetValue(SaveCommandProperty); }
set { SetValue(SaveCommandProperty, value); }
}
private static void OnLocationChanged(
DependencyObject source, DependencyPropertyChangedEventArgs e)
{
if (gpLocation != null)
{
gpLocation.Text = e.NewValue.ToString();
}
}
private static void OnGelPakSourceChanged(
DependencyObject source, DependencyPropertyChangedEventArgs e)
{
if (gpSelector != null)
{
gpSelector.ItemsSource = (IEnumerable) e.NewValue;
}
}
private static void OnSaveCommandChanged(
DependencyObject source, DependencyPropertyChangedEventArgs e)
{
if (saveButton != null)
{
saveButton.Command = (ICommand) e.NewValue;
}
}
}
This is how it is referenced in the main window:
<ctl:GelPakPickerOverlay
Width="132"
DockPanel.Dock="Right"
VerticalAlignment="Bottom"
Background="{StaticResource primaryBrush}"
BorderBrush="{StaticResource accentBrushOne}"
BorderThickness="2,2,0,0"
Visibility="{
Binding GelPakPickerViewModel.IsPickerVisible,
Converter={StaticResource BoolToHiddenVisConverter},
FallbackValue=Visible}"
GelPakSource="{Binding GelPakPickerViewModel.GelPakList}"
SelectedGelPak="{Binding GelPakPickerViewModel.SelectedGelPak}"
Location="{Binding GelPakPickerViewModel.GelPakLocation, UpdateSourceTrigger=LostFocus}"
SaveCommand="{Binding GelPakPickerViewModel.UpdateGpDataCommand}"/>
The data context of this window is MainWindowViewModel which has a GelPakPickerViewModel property which all of the bindings are hooked up to. The "Location", "GelPakSource" and "SaveCommand" properties all work correctly and routes everything to the GelPakPickerViewModel the way I expect. However, when you select anything from the combo box, it never actually makes it into the GelPakViewModels SelectedGelPak property (which is of type GelPak).
What is going on here? Does anyone have any suggestions to fix this issue?!?
EDIT: I added a property changed event listener to the SelectedGelPakProperty like this:
public static readonly DependencyProperty SelectedGelPakProperty =
DependencyProperty.Register(
"SelectedGelPak",
typeof(object),
typeof(GelPakPickerOverlay),
new FrameworkPropertyMetadata(null, OnSelectedGelPakChanged));
........
private static void OnSelectedGelPakChanged(
DependencyObject source, DependencyPropertyChangedEventArgs e)
{
if (gpLocation != null)
{
gpSelector.SelectedItem = e.NewValue;
}
}
but this still doesn't actually change the SelectedGelPak object in the view model.
Upvotes: 1
Views: 1322
Reputation: 4978
You're listening to changes on the property of your ViewModel, but that's just one of the ways in which the data is flowing. You need to listen to changes in the view, in your Combo.
To do so, subscribe to its SelectionChanged
event like this:
gpSelector = new ComboBox();
gpSelector.Width = 100;
gpSelector.Margin = new Thickness(10);
gpSelector.HorizontalAlignment = HorizontalAlignment.Left;
gpSelector.VerticalAlignment = VerticalAlignment.Center;
gpSelector.SelectionChanged += OnGpSelectorSelectionChanged;
And then in your event handler, change the value of your DependencyProperty accordingly:
private void OnGpSelectorSelectionChanged(object sender, SelectionChangedEventArgs e)
{
SetCurrentValue(SelectedGelPakProperty, gpSelector.SelectedItem);
}
This way you're supporting the two-way communication between ViewModel and Control.
Upvotes: 2
Reputation: 128060
The SelectedGelPak
binding should be two-way.
Either you set the Binding.Mode
property, like
SelectedGelPak="{Binding GelPakPickerViewModel.SelectedGelPak, Mode=TwoWay}"
or you make the SelectedGelPak
property bind two-way by default, by setting the corresponding flag in the property metadata:
public static readonly DependencyProperty SelectedGelPakProperty =
DependencyProperty.Register(
"SelectedGelPak",
typeof(object),
typeof(GelPakPickerOverlay),
new FrameworkPropertyMetadata(
null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); // here
EDIT: Instead of having a PropertyChangedCallback (OnSelectedGelPakChanged
), you may now directly bind the SelectedItem
property of the internal ComboBox to the SelectedGelPak
property:
<ComboBox ... SelectedItem="{Binding SelectedGelPak,
RelativeSource={RelativeSource AncestorType=local:GelPakPickerOverlay}}"/>
Upvotes: 1
Reputation: 3448
You do not specify any action to assign when SelectedGelPak changes its value (only FrameworkPropertyMetadataOptions.BindsTwoWayByDefault). Add
new FrameworkPropertyMetadata(null, OnSelectedGelPakChanged));
and in this method assign SelectedGelPak to gpSelector.SelectedItem
EDIT:
Honestly, your code looks nasty since you placed in one class both visual declaration and logic. You have .xaml file to declare how your class looks like and .xaml.cs for some logic. Separate then them as follows:
XAML:
<StackPanel Name="MainPanel">
<ComboBox SelectedItem="{Binding SelectedGelPak}"
ItemsSource="{Binding GelPakSource}"/>
<TextBox Text="{Binding Location}"/>
<Button Command="{Binding SaveCommand}"/>
</StackPanel>
.XAML.CS:
public static readonly DependencyProperty SelectedGelPakProperty =
DependencyProperty.Register(
"SelectedGelPak",
typeof(object),
typeof(GelPakPickerOverlay),
new FrameworkPropertyMetadata(
null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static readonly DependencyProperty LocationProperty =
DependencyProperty.Register(
"Location",
typeof(string),
typeof(GelPakPickerOverlay),
new FrameworkPropertyMetadata(string.Empty, OnLocationChanged));
public static readonly DependencyProperty GelPakSourceProperty =
DependencyProperty.Register(
"GelPakSource",
typeof(IEnumerable),
typeof(GelPakPickerOverlay),
new FrameworkPropertyMetadata(null, OnGelPakSourceChanged));
public static readonly DependencyProperty SaveCommandProperty =
DependencyProperty.Register(
"SaveCommand",
typeof(ICommand),
typeof(GelPakPickerOverlay),
new FrameworkPropertyMetadata(null, OnSaveCommandChanged));
public GelPakPickerOverlay()
{
this.MainPanel.DataContext = this;
}
public object SelectedGelPak
{
get { return GetValue(SelectedGelPakProperty); }
set { SetValue(SelectedGelPakProperty, value); }
}
public string Location
{
get { return GetValue(LocationProperty).ToString(); }
set { SetValue(LocationProperty, value); }
}
public IEnumerable GelPakSource
{
get { return (IEnumerable) GetValue(GelPakSourceProperty); }
set { SetValue(GelPakSourceProperty, value); }
}
public ICommand SaveCommand
{
get { return (ICommand) GetValue(SaveCommandProperty); }
set { SetValue(SaveCommandProperty, value); }
}
}
Crucial in this case is constructor. Your StackPanel's DataContext points to your code-behind file so elements within StackPanel have effortless access to declared Dependency Properties but in turn DataContext of the whole GelPakPickerOverlay still bases on parent's so nothing has changed. Try it.
Upvotes: 0