WPF, property bound to dependent ComboBox always gives initial value - c#

I have a ComboBox that needs to depend on the value of another ComboBox. This part already works, with the dependent ComboBox refreshing when a new value is chosen in the independent ComboBox:
<!-- Independent -->
<ComboBox Height="23" HorizontalAlignment="Left" Grid.Row="2" Grid.Column="2"
x:Name="cbo_product" VerticalAlignment="Center" Width="120"
ItemsSource="{Binding Source={StaticResource productsXml}}"
DisplayMemberPath="#name" SelectedValuePath="#name"
SelectionChanged="cbo_product_SelectionChanged"
SelectedValue="{Binding Path=Product}" />
<!-- Dependent -->
<ComboBox Height="23" HorizontalAlignment="Left" Grid.Row="3" Grid.Column="2"
x:Name="cbo_component" VerticalAlignment="Center" Width="201"
DataContext="{Binding SelectedItem, ElementName=cbo_product}"
ItemsSource="{Binding XPath=Components/Component}"
DisplayMemberPath="#name" SelectedValuePath="#name"
SelectedValue="{Binding Path=Component}"
SelectionChanged="cbo_component_SelectionChanged" />
In the C# class behind this, I have:
public MyUserControlConstructor()
{
MyViewModelInstance= new MyViewModel();
DataContext = MyViewModelInstance;
}
And in MyViewModel, I have:
public string Component
{
get { return _component; }
set
{
if (value == _component)
{
return;
}
_component = value;
onPropertyChanged(PropertyNames.Component);
}
}
private void onPropertyChanged(PropertyNames fieldName)
{
if (null == PropertyChanged)
{
return;
}
string propertyName = Enum.GetName(typeof(PropertyNames), fieldName);
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
When I change the dependent ComboBox (Component), it shows up with the new value in my app, of course. However, when I hit a button that causes the value of the Component property to be displayed, it is always the initial value, and not the value I just chose in the ComboBox. I think there must be an error in my XAML. For the C#, I tried to follow a combination of this and this guide. How do I tie my dependent ComboBox to XML values nested in the SelectedItem of the independent ComboBox, but still update the Component property in my class?
Edit: my suspicion is that things are wonky because I set the DataContext for the dependent ComboBox in two places: first in the constructor in C#, to my view model, and second in the XAML, to DataContext="{Binding SelectedItem, ElementName=cbo_product}".
Edit: I had been setting initial values in the constructor to my view model class. When I take out the initial value for the Component property, then even after I change the selected value in the dependent ComboBox, I still get no value from the Component property. This pretty much just rehashes what I already knew: the dependent ComboBox is tied to the independent ComboBox (it gets its data from the independent ComboBox, that is), but not to the Component property.
Edit: by request, here's a sample of my XML:
<Products xmlns="">
<Product name="Awesomeness">
<Components>
<Component name="Component of Glory"/>
<Component name="Component of Doom"/>
</Components>
</Product>
</Products>
Edit: I'm guessing a MultiBinding would be of use, after looking at this and this.
Edit: it seems like I should be able to get the dependent ComboBox to work without setting DataContext, just by using ItemsSource:
<ComboBox Height="23" HorizontalAlignment="Left" Grid.Row="3" Grid.Column="2"
x:Name="cbo_component" VerticalAlignment="Center" Width="201"
ItemsSource="{Binding ElementName=cbo_product, Path=SelectedItem,
XPath=Components/Component}"
DisplayMemberPath="#name" SelectedValuePath="#name"
SelectedValue="{Binding Path=Component}"
SelectionChanged="cbo_component_SelectionChanged"/>
However, this doesn't work: the dependent ComboBox is empty, instead of showing all the Component names.

The way I found of getting around this involves setting the ItemsSource in C# instead of XAML, which I would prefer not to do. However, it works, and after a day and a half of banging on this, it's the best I came up with.
In XAML:
<!-- Independent -->
<ComboBox Height="23" HorizontalAlignment="Left" Grid.Row="2" Grid.Column="2"
x:Name="cbo_product" VerticalAlignment="Center" Width="120"
ItemsSource="{Binding Source={StaticResource productsXml}}"
DisplayMemberPath="#name" SelectedValuePath="#name"
SelectionChanged="cbo_product_SelectionChanged"
SelectedItem="{Binding Path=ProductNode}"
SelectedValue="{Binding Path=Product}" />
<!-- Dependent -->
<ComboBox Height="23" HorizontalAlignment="Left" Grid.Row="3" Grid.Column="2"
x:Name="cbo_component" VerticalAlignment="Center" Width="201"
DisplayMemberPath="#name" SelectedValuePath="#name"
SelectedValue="{Binding Path=Component, Mode=TwoWay}"
SelectionChanged="cbo_component_SelectionChanged"/>
In C#, the event handler for when the independent ComboBox changes:
private void cbo_product_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
// Set ItemsSource of dependent ComboBox
cbo_component.ItemsSource = getChildNodesFromComboBox(
sender as ComboBox, "Components/Component"
);
cbo_component.SelectedIndex = 0;
}
// Helper method to do XPath query and get child nodes from selected value of
// independent ComboBox
private XmlNodeList getChildNodesFromComboBox(ComboBox comboBox,
string xpath)
{
if (null == comboBox)
{
return null;
}
var xml = comboBox.SelectedItem as XmlElement;
if (null == xml)
{
return null;
}
return xml.SelectNodes(xpath);
}
Now the Component property in my view model class, to which my dependent ComboBox is bound in XAML, gets populated with the value selected in the dependent ComboBox because I didn't have to change the DataContext of the dependent ComboBox.

Related

UWP x:bind issue - Invalid binding path 'dpl' : Property 'dpl' not found on type 'DataTemplate'

I have a class PricingData and PricingSchedule. Where PricingSchedule is a List<> inside PricingData class. I want to bind data of this class to UWP controls.
Sample code is available to download here : https://github.com/jigneshdesai/SampleOfBindingIssue1.git
How Code looks: i have a start page(mainpage) that hosts ListView control, Listview has PricingUserControl within it. PricingUserControl looks like this
<TextBlock x:Name="lblPriceHeader" Text="{Binding PricingTitle}" Margin="0,0,50,0" />
<ComboBox x:Name="cbPriceValueList" ItemsSource="{x:Bind dpl}" DisplayMemberPath="PriceValue" SelectedValuePath="PriceValue" SelectedValue="{Binding DisplayPricing}" />
<ListView x:Name="lbPriceChangeSchedule" ItemsSource="{Binding PricingScheduleList}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<StackPanel Orientation="Horizontal">
<ComboBox x:Name="cbSchedulePriceValueList" ItemsSource="{x:Bind dpl}" DisplayMemberPath="PriceValue" SelectedValuePath="PriceValue" />
<TextBlock Text="{Binding SchedulePricingTimeZone }" />
</StackPanel>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
What i want to achieve: Combobox should populate a list of values (eg. 1USD, 2USD, 3USD etc.). Then when you provide List of records from database, the listbox will repeat PricingUserControl and combobox within it should set its value property (SelectedValue) as per record.
Issue:
ComboBox x:Name="cbPriceValueList" uses x:bind dpl where dpl is a local variable of PricingUserControl. It populates the list properly. The trouble is ComboBox x:Name="cbSchedulePriceValueList" it also has x:bind dpl but during compilation it display error "Invalid binding path 'dpl' : Property 'dpl' not found on type 'DataTemplate'."
I am wondering why x:bind dpl does not work at this point. ?
I have now realized that your problem is in fact that you need to reach to a Page property from within the DataTemplate, so here is a updated answer.
You cannot use x:Bind if you need to access an outside element's property from within a DataTemplate. Instead, you can use classic {Binding} expression. First add a name to your page:
<Page
...
x:Name="Page">
And now refer to this name from within the DataTemplate:
<ComboBox
x:Name="cbSchedulePriceValueList"
ItemsSource="{Binding ElementName=Page, Path=dpl}"
DisplayMemberPath="PriceValue"
SelectedValuePath="PriceValue" />
Original answer
To be able to use x:Bind inside of a DataTemplate, you must specify the data type the individual items of the control will have, using x:DataType. Suppose your PricingScheduleList is a List<MyApp.Models.MyType>, then you will first need to add this XML namespace to the <Page> element:
xmlns:models="using:MyApp.Models"
And then set the x:DataType attribute as follows:
<DataTemplate x:DataType="models:MyType">
...
</DataTemplate>
You can confirm this works by the fact that IntelliSense should now suggest you the properties of MyType when you start writing the x:Bind expression.
By checking your code, the reason why SelectedValue does not take effect is when you choose the item from ComboBox, you didn't notify your DisplayPricing to change. So you need to implement INotifyPropertyChanged interface in your PricingData. Do the same behavior in PricingSchedule.
public class PricingData : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
......
public string DisplayPricing
{
get => $"{PricingValue} {PricingCurrency}";
set
{
var sp = value.Split(' ');
PricingValue = sp.First();
PricingCurrency = sp.Last();
OnPropertyChanged();
}
}
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
PScheduleUserControl.xaml:
<ComboBox x:Name="cbPriceValueList" ItemsSource="{x:Bind myList}" DisplayMemberPath="PriceValue" SelectedValuePath="PriceValue" SelectedValue="{Binding DisplayPricing,Mode=TwoWay}" />

How to DataBind a label or a textBlock in Datagrid Cell?

Hello Guys I hope yall doing great!
I have a problem with a datagrid, I want to put this in datagrid cell for example : "/100" which '100' is the quantity in stock in my database using entity framework, I want to do this so the user tap how many items he wants in a textbox besides the label/textBlock like this :
DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBox materialDesign:HintAssist.Hint="0"/>
<TextBlock Text="{Binding ElementName=productQuantityStock}" />
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
I hope you guys understand what I want to do, and please mind my bad English
In the code-behind you'll need to bind a context. The context will need a property and a field so the the value can be updated. You should also make use of a method that notifies the view that the property has been updated.
The code behind will look something like this.
public class DataContextOfView
{
private int _productQuantityStock;
// Because you'll be working with values of type int you should make it an int
public int ProductQuantityStock
{
get { return _productQuantityStock;}
set { if(_productQuantityStock != value)
{
_productQuantityStock = value
// notify that the value of the property has changed.
OnPropertyChanged(nameof(ProductQuantityStock));
}
}
}
}
The code in the view should have a reference to the property
DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBox materialDesign:HintAssist.Hint="0"/>
<TextBlock Text="{Binding ProductQuantityStock, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>

Special Sorting ListView when clicking header

I got a row like this:
XAML:
<ListView x:Name="ListViewAnlagen"
Grid.RowSpan="2"
ItemContainerStyle="{StaticResource TempContainerStyle}"
VerticalAlignment="Top" HorizontalAlignment="Left"
Height="571" Width="1314"
Margin="0,53,0,0"
AlternationCount="2"
GridViewColumnHeader.Click="GridViewColumnHeaderClickedHandler">
<ListView.View>
<GridView ColumnHeaderContainerStyle="{DynamicResource CustomHeaderStyle}">
<GridView.Columns>
<GridViewColumn Width="100">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Border x:Name="border"
BorderBrush="Gray" BorderThickness=".5" Margin="-6,-3">
<TextBlock Text="{Binding EqNr}" Margin="6,3"/>
</Border>
</DataTemplate>
</GridViewColumn.CellTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="EQ Nr."/>
<Image Source="img/filter.png"
Width="20" Height="20" Margin="25 0 0 0"
MouseDown="Image_MouseDown_1" />
</StackPanel>
</GridViewColumn>
I have added a click handler: GridViewColumnHeader.Click
My Question is, how to sort this ascending and descending. I already looked at some others solutions, but it seems they only work when you bind it with DisplayMemberBinding.
What I already tried:
this
Since you already examined the example as commented by #AmolBavannavar (https://code.msdn.microsoft.com/windowsdesktop/Sorting-a-WPF-ListView-by-209a7d45), here is a hybris between the example and your current approach.
The main obstacle in adapting the example is the usage of GridViewColumnHeader.Command and GridViewColumnHeader.CommandParameter. Your equivalent for the command is the GridViewColumnHeader.Click="GridViewColumnHeaderClickedHandler", but you still need an equivalent to the command parameter.
I suggest you create an attached string property for this purpose and use it to attach the sort property name to the GridViewColumn. For the sake of demonstration, I don't create a new property but instead misuse the TextSearch.TextPath attached property:
<GridViewColumn Width="100" TextSearch.TextPath="EqNr">
Note that the "EqNr" is the same as the property name that is used for binding inside the cell template later.
Now, everything is in place to be used inside the click handler.
Get the clicked column header
Get the associated column
Get the attached property value that contains the sort property name
Get the collection view that is associated with the items source (or items)
Change the sort descriptions of the collection view
Code with simplified sorting logic:
private void GridViewColumnHeaderClickedHandler(object sender, RoutedEventArgs e)
{
var h = e.OriginalSource as GridViewColumnHeader;
if (h != null)
{
var propertyName = h.Column.GetValue(TextSearch.TextPathProperty) as string;
var cvs = ListViewAnlagen.ItemsSource as ICollectionView ??
CollectionViewSource.GetDefaultView(ListViewAnlagen.ItemsSource) ??
ListViewAnlagen.Items;
if (cvs != null)
{
cvs.SortDescriptions.Clear();
cvs.SortDescriptions.Add(new SortDescription(propertyName, ListSortDirection.Descending));
}
}
}
Note that for the sake of demonstration I only clear the sort descriptions and add a static descending sort description. For your actual application, you may want to keep track (or analyze) the current sorting status for the column and then alternate between ascending and descending sort.

WPF ComboBox SelectionItemBox prompt string

So I searched in this forum but couldn't find the anwser...
I'm Binding my combobox to a list of users. But when I want to type in it for example a name, some wierd stuff is comming out.
XAML Code:
<ComboBox Name="UserLoginCmbBox" Width="100" IsEditable="True" SelectionChanged="UserLoginCmbBox_SelectionChanged">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Login}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Code Behind Code:
var users = new List<User>{...} //initialization
UserLoginCmbBox.ItemsSource = users;
When I'm typing in the box only string I can get is:
System.Data.Entity.DynamicProxies.User_FE59351B6479342209B496E7D3C3B877DDC51FE15279FFFEF899A5012F85FCBA
I found out that this is what ToString() method returns, but what if I want to use the same list for expamle in another ComboBox where I want to choose the Name or something else?
How can I modify SelectionItemBox? Cause when I tried to use the SelectionItemBoxTemplate to write my own template, an error occured (SelectionItemBoxTemplate has only getter).
Try to set the DisplayMemberPath property to "Login":
<ComboBox Name="UserLoginCmbBox" Width="100" IsEditable="True" SelectionChanged="UserLoginCmbBox_SelectionChanged" DisplayMemberPath="Login" />

Why does my dropdown feel so clunky?

I have a XAML UserControl embedded in a WinForms/WPF Interop ElementHost control. The control is pretty simple - it's just a dropdown with a button - here's the entire markup:
<UserControl x:Class="Rubberduck.UI.FindSymbol.FindSymbolControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Rubberduck.UI.FindSymbol"
mc:Ignorable="d"
d:DesignHeight="27" d:DesignWidth="270">
<UserControl.Resources>
<local:DeclarationImageConverter x:Key="DeclarationImageConverter" />
</UserControl.Resources>
<UserControl.CommandBindings>
<CommandBinding Command="local:FindSymbolControl.GoCommand"
Executed="CommandBinding_OnExecuted"
CanExecute="CommandBinding_OnCanExecute"/>
</UserControl.CommandBindings>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="32" />
</Grid.ColumnDefinitions>
<ComboBox IsEditable="True"
ItemsSource="{Binding MatchResults}"
SelectedItem="{Binding SelectedItem, UpdateSourceTrigger=PropertyChanged}"
Text="{Binding SearchString, UpdateSourceTrigger=PropertyChanged}"
IsTextSearchCaseSensitive="False"
IsTextSearchEnabled="True"
TextSearch.TextPath="IdentifierName">
<ComboBox.ItemTemplate>
<DataTemplate DataType="local:SearchResult">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Image Height="16" Width="16" Margin="2,0,2,0" Source="{Binding Declaration, Converter={StaticResource DeclarationImageConverter}}" />
<TextBlock Margin="2,0,2,0" Text="{Binding IdentifierName}" FontWeight="Bold" MinWidth="140" />
<TextBlock Margin="2,0,2,0" Text="{Binding Location}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button Grid.Column="1"
Command="local:FindSymbolControl.GoCommand">
<Image Height="16" Source="pack://application:,,,/Rubberduck;component/Resources/arrow.png" />
</Button>
</Grid>
</UserControl>
The problem is that it doesn't work reliably, and far from instinctively.
If I type something in the box that actually matches an item, nothing happens until I manually select that item in the dropdown. Like here, I typed "sleepD", the box autocompleted to "sleepDelay", but the command is still disabled:
Once I've selected the item in the dropdown, the command button gets enabled as expected (although the image on the button doesn't show up grayed-out when the button is disabled, so it's not exactly as obvious as I intended it to be).
(the screenshot isn't really showing it, but there's only 1 match for that search)
If I click the button at that point, it works as expected. The problem is that if I make a new selection from the dropdown after that, the text box gets cleared instead of displaying the item I selected, and there's a weird delay during which the box is displaying what appears to be selected whitespace - this only seems to happen when the previous selection was made after selecting a value in the dropdown while the search text matches multiple entries, like "Sleep" above.
After the box got cleared, I can make a new selection from the dropdown and it will work as expected (except the VBE won't actually activate the CodePane I'm setting the selection to, but that's a separate issue).
The command implementation simply raises a Navigate event that passes a Declaration to the code that owns the VM instance.
The Search method, for which I need to add a .Take(50) after the .Select, to limit the number of returned results and perhaps reduce the lag a bit:
private void Search(string value)
{
var lower = value.ToLowerInvariant();
var results = _declarations.Where(
declaration => declaration.IdentifierName.ToLowerInvariant().Contains(lower))
.OrderBy(declaration => declaration.IdentifierName.ToLowerInvariant())
.Select(declaration => new SearchResult(declaration));
MatchResults = new ObservableCollection<SearchResult>(results);
}
private string _searchString;
public string SearchString
{
get { return _searchString; }
set
{
_searchString = value;
Search(value);
}
}
private SearchResult _selectedItem;
public SearchResult SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
OnPropertyChanged();
}
}
private ObservableCollection<SearchResult> _matchResults;
public ObservableCollection<SearchResult> MatchResults
{
get { return _matchResults; }
set { _matchResults = value; OnPropertyChanged(); }
}
}
There's also an IValueConverter involved, that takes the Declaration in the SearchResult and switches on the declaration's DeclarationType enum to return a pack uri that points to the .png image to use in the dropdown list.
Aaah found it. It was all in the XAML.
Right here:
Text="{Binding SearchString, UpdateSourceTrigger=PropertyChanged}"
That line doesn't belong there; binding the TextSearch.Text property instead...
TextSearch.Text="{Binding SearchString, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged}"
Makes it all work as intended. No glitch, no lag. Well there is a lag when I first drop the dropdown, but that's another issue.
Lesson learned: when TextSearch is enabled on an editable combobox, don't bind the Text property, unless you want weird behavior.

Categories

Resources