WPF ListView binding to the collection - c#

I implemented a collection of Hyperlink elements using WPF:
var controlLinks = new List<Hyperlink>();
if (issueLinks != null)
{
foreach (var link in issueLinks)
{
var tempLink = new Hyperlink()
{
NavigateUri = new Uri(link.link)
};
controlLinks.Add(tempLink);
}
}
ListIssueLinks.ItemsSource = controlLinks;
Collections is successfuly filled, now I link ListIssueLinks view to this collection.
<ListView Name="ListIssueLinks" Height="100" >
<ListView.View>
<GridView>
<GridViewColumn/>
</GridView>
</ListView.View>
</ListView>
Here I've got a problem, the issue is I'm new to WPF and have no idea how properly implement formatting (for example, to present NavigateUri or Name only on UI) and implement generic handler for click on any element. Something like this:
private void Hyperlink_OnClick(object sender, RoutedEventArgs e)
{
var clickedLink = (Hyperlink) sender;
System.Diagnostics.Process.Start(clickedLink.NavigateUri.ToString());
}
I tried DataTemplate, tried a lot of other variants, googled really a lot, but still have no clue how to do that. Could you please suggest any easy and elegant solution?

DataTemplate is your best bet, here's how you could implement it in your case.
<ListView Name="ListIssueLinks" Height="100">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock>
<Hyperlink NavigateUri="{Binding}" RequestNavigate="Link_RequestNavigate">
<TextBlock Text="{Binding}" />
</Hyperlink>
</TextBlock>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Then in your code behind, I would simply bind directly to your issueLinks (no need to build up HyperLinks in code).
List<string> issueLinks = new List<string>()
{
"http://www.google.com",
"http://www.stackoverflow.com",
};
ListIssueLinks.ItemsSource = issueLinks;
Then your RequestNavigate event handler needs to start your process
private void Link_RequestNavigate(object sender, RequestNavigateEventArgs e)
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(e.Uri.AbsoluteUri));
e.Handled = true;
}

I didnt fully understand what is your problem but i have two things that you should know that exist.
ObservableCollection
i think you should consider use it instead of a simple List because it has advantages if you need to bind the List to The View with your ListView for example. for more info read here: ObservableCollection Class
XAML side
the second thing i think you tried to explain is how to take properties and show them in your list view. this means you need to write it on the XAML side and explore about it farther, you can bind the properties after that with inotifypropertychanged if the data in the list is going to change with any reason.
hope i helped.

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>

UWP Get Clicked Item in DataTemplate

I'm sure this has been asked before, however I cannot find the appropriate answer.
I am using an ItemTemplate to display a list of users, this list consists of StackPanels with user details. How can I get the user object that I clicked on?
Current code:
<GridView Name="usersList">
<GridView.ItemTemplate>
<DataTemplate>
<StackPanel PointerPressed="UserClicked">
<TextBlock Text="{Binding Forename}"/>
<TextBlock Text="{Binding Surname}"/>
</StackPanel>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
This is being populated via an async API call:
usersList.ItemsSource = null;
var task = Users.UserAsync<User>();
usersList.ItemsSource = await task;
How can I capture the User object that has been clicked on PointerPressed?
Like this:
private void UserClicked(object sender, PointerRoutedEventArgs e)
{
User clickedOnUser = (sender as StackPanel).DataContext as User;
}
This approach is more direct than the one using usersList.SelectedItem, so I recommend sticking with it. E.g., it will work if you set SelectionMode="None" for the GridView, whereas the approach based on usersList.SelectedItem won't.
Also, as Sean O'Neil rightfully noticed, you might want to use Tapped event instead of PointerPressed for the most general case scenario.
Use GridView.SelectedItem to reference the object you want when you click on it.
private void UserClicked_PointerPressed(object sender, PointerRoutedEventArgs e)
{
var whatYouWant = usersList.SelectedItem;
}

c# How to get Listbox selection from Observable Collection

I'm probably not even asking this correctly, I am new to c#, but trying to help my 14 year-old son learn. I've created a listbox with items created with an ObservableCollection. Here is the XAML:
<ListBox x:Name="listBox1" ItemsSource="{Binding}" Margin="105,205,886,63"
IsTabStop="True" SelectionChanged="PrintText"
ScrollViewer.VerticalScrollBarVisibility="Hidden" TabIndex="5" FontSize="36"
Background="Transparent" Foreground="#FF55B64C" FontFamily="Arabic Typesetting"
FontWeight="Bold" IsDoubleTapEnabled="False" SelectionMode="Single" >
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Name="blockNameList" Text="{Binding name}"/>
<TextBlock Text=" #"/>
<TextBlock Name="blockIdList" Text="{Binding id}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Here is how I created the ListBox Items:
var client = new HttpClient();
var uri = new Uri("http://theurlImusing");
Stream respStream2 = await client.GetStreamAsync(uri);
// DataContractJsonSerializer ser2 = new DataContractJsonSerializer(typeof(RootObject));
// RootObject feed2 = (RootObject)ser2.ReadObject(respStream2);
DataContractJsonSerializer ser = null;
ser = new DataContractJsonSerializer(typeof(ObservableCollection<RootObject>));
ObservableCollection<RootObject> feed2 = ser.ReadObject(respStream2) as ObservableCollection<RootObject>;
var cardList = new List<RootObject>();
foreach (RootObject returnfeed in feed2)
{
string cid = returnfeed.id;
string cardname = returnfeed.name;
listBox1.Items.Add(new RootObject { id=cid, name=cardname });
}
I thought I would just use the SelectionChanged="PrintText" property of the listbox so that when I clicked on a listbox item, it would just change a textblock's text value. Ultimately, that is all I am trying to do...set a textblock or textbox to be equal to the "id" value that is clicked on in the ListBox.
void PrintText(object sender, SelectionChangedEventArgs args)
{
//What do I put in here??
}
Thanks very much for any insight! I need it!!
This is something that is much easier to do using data binding. You can bind the TextBlock.Text property directly to the ListBox using an ElementName binding:
<TextBox Text="{Binding ElementName=listBox1,Path=SelectedItem.id}" />
Alternatively, if you set set SelectedValuePath="id" on the ListBox, then binding to SelectedValue will give you the "id" property:
<ListBox x:Name="listBox1" SelectedValuePath="id" ... />
<TextBox Text="{Binding ElementName=listBox1,Path=SelectedValue}" />
As a side note (as #Rachel already noted in comments): you may as well just set the ItemsSource, rather than looping through and adding each manually. All you need is this:
listBox1.ItemsSource = feed2;
Edit
Ok, if you wanted to use the procedural approach, here's how you would do it. (No one would recommend this approach, especially if you're learning/teaching. Try to make full use of data binding, and view-viewmodel separation.)
void PrintText(object sender, SelectionChangedEventArgs args)
{
var listBox = (ListBox)sender;
RootObject selectedItem = listBox.SelectedItem;
someTextBox.Text = selectedItem.id;
}
If all you want to do is click an item in the ListBox and get it to show up in the TextBox, you don't need fancy binding (in that other answer) to do it. You can simply add a MouseUp event in the ListBox XAML:
MouseUp="ListBox1_MouseUp"
This would work similar to the SelectionChanged event you wanted to use.
You then right-click that function name in the XAML page and select "Go to definition". It will create the next function for you:
private void ListBox1_MouseUp(object sender, MouseButtonEventArgs e)
{
}
Simply add in there to update the TextBox you want with the SelectedItem values from sender:
private void ListBox1_MouseUp(object sender, MouseButtonEventArgs e)
{
ListBox lstBox = (ListBox)sender;
ListBoxItem item = lstBox.SelectedItem;
if (item != null) // avoids exception when an empty line is clicked
{
someBox.Text = item.name;
someOtherBox.Text = item.id;
}
}
I later found that blockNameList and blockIdList are not accessible via intellisense because they are within the DataTemplate of the ListBox, so I put someBox and someOtherBox, as references to other TextBoxes you would have to add to the XAML, outside of the ListBox. You would not re-write data inside the ListBox on the same item by clicking it. Even if you could reach the template's TextBlock to do it, you'd just be re-writing that same item with its own values, since it would be the SelectedItem!
Even though there are those that don't recommend this approach because they like binding everything - and in some cases you want binding to occur so that controls on the page update as a result of dependencies (i.e. do one thing to cause another), I find that manual methods of clicking a button/item/control to update something are just fine and avoid all the model/MVVM BS that has taken over WPF and over-complicated it.

How to get GridViewItem from its child element?

Suppose I have a code in XAML like this:
<GridView>
<GridView.ItemTemplate>
<DataTemplate>
<Button Content="{Binding test}" Click="ButtonClick" />
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
Then how can I get which GridViewItem was selected? Because, normally what is done is to add the ItemClick functionality to the GridView itself, but in this case I am doing something customized and need to get the SelectedItem starting from the Button.
I tried code something like this:
void ButtonClick (object sender, RoutedEventArgs e)
{
var g = (GridViewItem)((Button)sender).Parent;
}
But it does not work (returns null). Please help!
Thanks!
Sure! Here's the code that I use when the ad control fails to load an ad (like when the machine is offline). In that case I remove it form the gridview. To do that I have to locate the ad's parent gridviewitem and remove the whole thing. I do it like this:
private void AdControl_ErrorOccurred_1(object sender, Microsoft.Advertising.WinRT.UI.AdErrorEventArgs e)
{
var _Item = sender as DependencyObject;
while (!(_Item is GridViewItem))
_Item = VisualTreeHelper.GetParent(_Item);
HubGrid.Items.Remove(_Item);
}

WPF Binding: Where a property contains the path to the value

I've got an expander with a couple of TextBlocks in the top bar which i'm using to give a title and a piece of key information.
Ideally i want to set the path to key piece of information, but i can't work out how to bind the path of the binding to another path (i apologise if i'm not making much sense!)
In the following xaml the first bit works, the second bit is what i'm struggling with.
<TextBlock Text="{Binding Path=Header.Title}"/>
<TextBlock Text="{Binding Path={Binding Path=Header.KeyValuePath}}"/>
KeyValuePath might contain something like "Vehicle.Registration" or "Supplier.Name" depending on the Model.
Can anyone point me in the right direction please? Any help gratefully received!
I don't think it can be done in pure XAML... Path is not a DependencyProperty (and anyway Binding is not a DependencyObject), so it can't be the target of a binding
You could modify the binding in code-behind instead
I haven't found a way to do this in XAML but I did this in code behind. Here is the approach I took.
Firstly, I wanted to do this for all items in an ItemsControl. So I had XAML like this:
<ListBox x:Name="_events" ItemsSource="{Binding Path=Events}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type Events:EventViewModel}">
<TextBlock Name="ActualText" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Then, in code behind construction I subscribe to the ItemContainerGenerator:
InitializeComponent();
_events.ItemContainerGenerator.StatusChanged
+= OnItemContainerGeneratorStatusChanged;
This method looks like:
private void OnItemContainerGeneratorStatusChanged(object sender, EventArgs e)
{
if (_events.ItemContainerGenerator.Status!=GeneratorStatus.ContainersGenerated)
return;
for (int i = 0; i < _viewModel.Events.Count; i++)
{
// Get the container that wraps the item from ItemsSource
var item = (ListBoxItem)_events.ItemContainerGenerator.ContainerFromIndex(i);
// May be null if filtered
if (item == null)
continue;
// Find the target
var textBlock = item.FindByName("ActualText");
// Find the data item to which the data template was applied
var eventViewModel = (EventViewModel)textBlock.DataContext;
// This is the path I want to bind to
var path = eventViewModel.BindingPath;
// Create a binding
var binding = new Binding(path) { Source = eventViewModel };
textBlock.SetBinding(TextBlock.TextProperty, binding);
}
}
If you only have a single item to set the binding upon, then the code would be quite a bit simpler.
<TextBlock x:Name="_text" Name="ActualText" />
And in code behind:
var binding = new Binding(path) { Source = bindingSourceObject };
_text.SetBinding(TextBlock.TextProperty, binding);
Hope that helps someone.

Categories