Setting binding source properly in XAML - c#

I'd like to have a list of TextBlocks with ComboBoxes next to each of them.
The data source of ComboBoxes should be the same for every ComboBox. Each TextBlock however should contain sequent element of List
Both data source for ComboBoxs and TextBlocks are in my "settings" object. So I set DataContext of the whole window to this settings object.
Here's my problem:
Data source of TextBlock is: List called Fields, which is inside of an object called "Header" of type "Line" (which is of course inside settings object, which is my datacontext).
So, graphically:
settings(type: Settings) - Header(type: CsvLine) - Fields(type: List of string)
Now ComboBox. Data source of every ComboBox should be a List called Tags
Graphically:
settings(type: Settings) - Tags(type: List of string)
I don't know how I should point to these locations, I tried a lot of options, but none of them work. I see just a blank window.
Here's my code:
<Grid>
<StackPanel Orientation="Horizontal">
<ItemsControl ItemsSource="{Binding Headers}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Fields}"/>
<ComboBox ItemsSource="{Binding DataContext.Tags,
RelativeSource={RelativeSource AncestorType=ItemsControl}}">
</ComboBox>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
I have no idea what I should actually pass as ItemsSource to ItemsControl, because I think it should be common source for both TextBoxes and ComboBoxes, but their only common source is settings object - but i already set it as my DataContext.
I have used RelativeSource in ComboBox, but I'm not really sure what it's used for (although I read an article about it on MSDN). I don't know why but it's really hard for me to understand binding - I'm struggling to get anything working.
//EDIT:
Here's my Settings class - which is the type of my settings object:
public class Settings
{
public CsvLine AllHeaders1
{
get
{
return _allHeaders1;
}
}
public CsvLine _allHeaders1 = new CsvLine()
{
Fields = new List<string>()
{
"Header1" , "Header2" , "Header3"
}
};
private List<String> _tags;
public List<String> Tags
{
get
{
return new List<string>() { "Tag1", "Tag2", "Tag3", "Tag4", "Tag5" };
}
set
{
_tags = value;
}
}
}
And here's my CsvLine class:
public class CsvLine
{
public List<string> Fields = new List<string>();
public int LineNumber;
}

So, I'm not 100% sure of what it is you want, but the following should get you started.
Firstly, you need to ensure you bind to public properties - not public members - so the CsvLine.Fields member needs to be changed to public List<string> Fields { get { return _fields; } set { _fields = value; } }. Also not that, if you want changes in the settings object to be reflected in the UI, you will need to implement INotifyPropertyChanged.
Anyway, with this in place and assigned to the DataContext of the grid, the following will display a vertical list of text blocks (showing "Header 1", "Header 2", "Header 3") each with a combo box to the right containing the values "Tag1", "Tag2" ... "Tag5".
<Grid x:Name="SourceGrid">
<ItemsControl ItemsSource="{Binding Path=AllHeaders1.Fields}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding}" />
<ComboBox ItemsSource="{Binding ElementName=SourceGrid, Path=DataContext.Tags}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
Hope it helps.

Related

c# - UWP ListView displays incorrect items upon rapid scrolling when it has a DataTemplate

I have a ListView that is intended to show every product within a database, and it works for the most part, but when I scroll down by dragging the scroll bar, the bottom items end up being incorrect.
XAML Definition:
<ListView x:Name="lst_Products" VerticalAlignment="Top" HorizontalAlignment="Left" Margin="16,124,16,16" Width="300" ContainerContentChanging="lst_Products_ContainerContentChanging" Loaded="lst_Products_Loaded" BorderBrush="Black" BorderThickness="2" CornerRadius="16">
<ListView.ItemTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding Value}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The data template is present so I can easily grab a product ID number with SelectedValue. According to some trusted community member (or whatever they call the prominent posters) on the MSDN forums said that's the only way to properly show a ListView when the ItemsSource is an ObservableCollection<KeyValuePair<int,RelativePanel>> while having a selectable value member.
The relevant C# code:
private async void lst_Products_Loaded(object sender, RoutedEventArgs e)
{
var products = await ProductManager.GetProducts();
ObservableCollection<KeyValuePair<int, RelativePanel>> productList = new(products);
lst_Products.ItemsSource = productList;
lst_Products.SelectedValuePath = "Key";
}
private void lst_Products_ContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
{
if (args.ItemIndex % 2 == 1)
{
args.ItemContainer.Background = new SolidColorBrush(Color.FromArgb(128, 128, 128, 128));
}
else
{
args.ItemContainer.Background = UIManager.GetDefaultBackground();
}
}
public static async Task<List<KeyValuePair<int, RelativePanel>>> GetProducts()
{
var productPanels = new List<KeyValuePair<int, RelativePanel>>();
var productIDs = await SqlHandler.ReturnListQuery<int>($"SELECT id FROM {productTable}");
var productNames = await SqlHandler.ReturnListQuery<string>($"SELECT name FROM {productTable}");
var panels = new List<RelativePanel>();
foreach(var name in productNames)
{
RelativePanel panel = new();
TextBlock productName = new()
{
Text = name
};
panel.Children.Add(productName);
panels.Add(panel);
}
for(int i = 0; i < productIDs.Count; i++)
{
productPanels.Add(new KeyValuePair<int, string>(productIDs[i], panels[i]));
}
return productPanels;
}
The call to SQL Handler just runs an SQL query and returns a list of the results. I can post the code if you need, but I can assure you there's no sorting going on.
A screenshot of what the list looks like. The bottom item should be "Coffee" - Button Test Product 2 is the second item in the list.
A screenshot of the SQL datatable with the "Coffee" product at the bottom where it should be.
In this case it's just the bottom item that's incorrect, however other times it has jumbled 5 or 6 entries near the bottom. This only seems to occur with the DataTemplate/ContentPresenter, but without that, the RelativePanel does not display correctly in the list. Eventually the list will show more information about the product and as far as I can tell, there's no good way to do that without converting the SQL data into a RelativePanel on the c# side.
I'm open to suggestions on solving either the jumbling problem with the template, or adjusting the xaml so that I don't need the template to display bulk sql data without needing the template but I'm at a loss.
c# - UWP ListView displays incorrect items upon rapid scrolling when it has a DataTemplate
The problem should be caused by listview virtualization, There are two ways to sloved this prolbem, one is disalbe listview virtualization by setting ItemsPanel as StackPanel like the following
<ListView>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
And the other way is implement INotifyCollectionChanged interface for your model class. for more please refer to Data binding in depth
It's not good practice that useRelativePanel collection as datasoure, the better way is make RelativePanel in your DataTemplate and bind with mode class property.
For example
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Index}" />
<TextBlock Text="{Binding IsItem}" />
<Image Source="{Binding ImageSource}" Visibility="Collapsed" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>

Text block not Updated

I added one text block and bind the first item of array to that text block. I called some API to get the data for that array. But the text block will not be updated when adding values to that array. When calling API it takes some time to get the data, at that time the Text block is rendered. So, After the text block rendered the UI is not updated.
XAML:
<TextBlock Text="{Binding Path=ItemSource[0], UpdateSourceTrigger
=PropertyChanged}" />
View Model:
await this.MyMethod();
this.ItemSource[0] = "Test After";
In order to bind a number of TextBlocks to a modifiable collection of strings, you could easily use an ItemsControl with a view model like this:
public class ViewModel
{
public ObservableCollection<string> Items { get; }
= new ObservableCollection<string>(
Enumerable
.Range(1, 20)
.Select(i => i.ToString())); // or any other initial values
}
The MainWindow constructor
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
In XAML use an ItemsControl:
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Now something like
((ViewModel)DataContext).Items[0] = "Hello";
would replace the first string in the collection and hence update the ItemsControl.

C# - Using WPF to create a Two-line ListBox for each item

Im having some trouble with WPF, as its my first time usage of it.
Im trying to create a ListBox, that holds two lines for each item in it. How can i achieve this ?
I have tried the following:
<ListBox>
<Label name="first">First Line</label>
<Label name="second">Second Line</label>
</ListBox>
Even though this does not give any errors, i do not think its the correct way to do it.
Can you guys assist ?
You can achieve this by modifying the ListBox ItemTemplate, while binding to a collection of data you want to display.
Xaml:
<ListBox Name="MyListBox" ItemsSource="{Binding ListBoxData}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Text1}" MinWidth="200"/>
<TextBlock Text="{Binding Text2}" MinWidth="200"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Xaml.cs:
namespace WpfApplication1
{
public partial class MainWindow
{
public List<MyRow> ListBoxData { get; set; }
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
ListBoxData = new List<MyRow>
{
new MyRow{Text1 = "Row 1 - Data 1", Text2 = "Row 1 - Data 2"},
new MyRow{Text1 = "Row 2 - Data 1", Text2 = "Row 2 - Data 2"},
new MyRow{Text1 = "Row 3 - Data 1", Text2 = "Row 3 - Data 2"}
};
}
}
public class MyRow
{
public string Text1 { get; set; }
public string Text2 { get; set; }
}
}
In terms of WPF, you typically want to use binding rather than hard-coding items into the xaml directly. The example above shows data binding via code-behind, but ideally, you would want to create a ViewModel and bind to that. I would suggest looking up MVVM once you get more familiar w/ WPF.
You can create items in a list box like so,
<ListBox>
<ListBoxItem Name="Item1">Item</ListBoxItem>
<ListBoxItem Name="Item2">Item2</ListBoxItem>
</ListBox>
Or if you want them dynamically assigned you can set the value like so in code,
Listbox1.ItemsSource = <some collection>;
Listbox1.DisplayMemberPath = "<Collection item you want displayed>";
Or this way for MVVM pattern
<ListBox ItemsSource="{Binding Path=<Your Property>}" DisplayMemeberPath="{Binding Path =Display Value Property}" />

Binding resp. DataTemplate confusion

I am still very new and trying my first serious data binding. I have read a lot about how it works, am just struggling with this concrete example. I have tried to read all links I could find on this, but most sources tend to be a bit imprecise at key spots. So here goes:
-My Application generates dynamically a variable 'PlayerList' of type 'List', where 'Player' is a complex object.
-I want to display this in a ListBox via Binding. Obvoiusly, since Player is a complex Object I want to create a DataTemplate for it. So I have something like this in the 'Window1.xaml':
<ListBox
Name="ListBox_Players"
ItemsSource="{Binding Source={StaticResource PlayerListResource}}"
ItemTemplate="{StaticResource PlayerTemplate}">
</ListBox>
and something like this in the 'App.xaml':
<DataTemplate x:Key="PlayerTemplate"> <!-- DataType="{x:Type Player}" -->
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=name}"/>
<TextBlock Text=", "/>
<TextBlock Text="{Binding Path=nrOfTabls}"/>
</StackPanel>
</DataTemplate>
Of course, this template will become more verbose later. So as you see above, I have tried to create a resource for the PlayerList variable, but have not managed yet, i.e., smthn. like this
<src: XXX x:Key="PlayerListResource"/>
where for XXX as I understand it I should enter the class of the Resource variable. I tried
List<Player>, List<src:Player>
etc., but obv. XAML has trouble with the '<,>' characters.
I also have another problem: By not declaring a resource but by direct binding (i.e., in C# writing "ListBox_Players.ItemsSource=PlayerList;") and deleting the 'ItemTemplate' declaration and overwriting the ToString() method of the Player class to output the name of the Player I have managed to see that the binding works (i.e., I get a list of Player names in the ListBox). But then, if I insert the template again, it displays only ','my Template does not work!
The fact that you're getting just commas without anything else suggests to me that either the names of Player members do not match the names in Path= in the DataTemplate (I had this problem at one point), or the relevant Player members are inaccessible.
I just tested what you've shown of your code so far, and it seemed to work fine. The only change I made was change this line:
ItemsSource="{Binding Source={StaticResource PlayerListResource}}"
to this line:
ItemsSource = "{Binding}"
This tells the program that it'll get the ItemsSource at run time.
My Player class was:
class Player {
public string name { get; set; }
public int nrOfTabls { get; set; }
}
and my MainWindow.xaml.cs was:
public partial class MainWindow : Window {
private ObservableCollection<Player> players_;
public MainWindow() {
InitializeComponent();
players_ =new ObservableCollection<Player> () {
new Player() {
name = "Alex",
nrOfTabls = 1,
},
new Player() {
name = "Brett",
nrOfTabls = 2,
},
new Player() {
name="Cindy",
nrOfTabls = 231,
}
};
ListBox_Players.ItemsSource = players_;
}
}

How to bind an object to a ComboBox, but update a different object when it changes?

I have a Zone object that contains
public int Block {get;set;}
I also have a configuration object which contains minimum and maximum Block values, which are 0 and 2 respectively.
I need to display a ComboBox with the range of valid values, but I need to have the selected value bound to Block.
What's the best way for me to do this?
I've been trying the following:
var blocks = new Dictionary<string, int>();
for (int i = _currentZone.Constraints.Block.Min; i <= _currentZone.Constraints.Block.Max; i++)
{
blocks.Add("Block " + i, i);
}
var blocksCombo = new ComboBoxControl(blocks, GetCurrentBlockValue());
with ComboBoxControl defined as
public ComboBoxControl(Dictionary<string, int> comboItems, int? selectedValue)
{
InitializeComponent();
cboItems.ItemsSource = comboItems;
cboItems.SelectedValue = selectedValue;
}
and the XAML defined as
<Grid>
<ComboBox x:Name="cboItems"
SelectionChanged="combo_SelectionChanged"
Height="25"
SelectedValuePath="Value">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Key}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
When the combo_SelectionChanged event is triggered I manually update the Block value, which isn't ideal.
What I'd like is to be able to set the combo box with the items in the dictionary, but when I change the selected item the value is bound to a different object - the Block. Is this possible?
If so, how can I implement this? If not, is there a better way for me to go about this than what I'm currently doing?
I believe it's as simple as changing you xaml to have...
<ComboBox x:Name="cboItems"
SelectionChanged="combo_SelectionChanged"
Height="25"
SelectedValuePath="Value"
SelectedItem="{Binding Block}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Key}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Assuming the data context is setup correctly, you probably need to set the datacontext of the combobox to your Zone object at some point, maybe pass it along with the constructor...
var blocksCombo = new ComboBoxControl(blocks, GetCurrentBlockValue(), this);
public ComboBoxControl(Dictionary<string, int> comboItems, int? selectedValue, Zone zone)
{
InitializeComponent();
cboItems.ItemsSource = comboItems;
cboItems.SelectedValue = selectedValue;
cboItems.DataContext = zone;
}
edit:
Also I think Henk is right, you might want to change the dictionary to instead be a ObservableCollection of Block. (actually just realized block is just an int, this will probably work as a dictionary)
I hope I understood everything right. You have the combobox and want to bind to one specific zone?
<ComboBox ItemsSource="{Binding ValidValuesList}" ItemStringFormat="Block {0}" SelectedItem="{Binding MyZone.Block}"/>
This binds to
public List<int> ValidValuesList
{
get { return new List<int> { 0, 1, 2 }; }
}
and to
public Zone MyZone { get; set; }
in your usercontrols DataContext.

Categories