Why isn't my ItemsControl showing my ViewModels? - wpf

I am trying to populate a page with a list of items from my database. I have created models and viewmodels for these items, but they are not showing up in my ItemsControl.
I have a Model, which has a corresponding ViewModel that implements INotifyPropertyChanged.
Model:
Public Class ItemModel
Private _year As String
Public Property Year As String
Get
Return _year
End Get
Set(value As String)
_year = value
End Set
End Property
Public Sub New()
_year = Now.Year & "-" & (Now.Year + 1)
End Sub
Public Function ToString() As String
Return _year & " Item Model"
End Function
End Class
ViewModel:
Imports System.ComponentModel
Public Class ItemViewModel
Implements INotifyPropertyChanged
Private _currentItem As ItemModel
Public Property CurrentItem As ItemModel
Get
Return _currentItem
End Get
Set(value As ItemModel)
If _currentItem IsNot value Then
_currentItem = value
NotifyPropertyChanged("CurrentItem")
End If
End Set
End Property
Public Sub New()
_currentItem = New DciSurveyModel()
End Sub
Public Function ToString() As String
Return _currentItem.Year & " Item ViewModel"
End Function
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Private Sub NotifyPropertyChanged(Optional ByVal propertyName As String = Nothing)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
End Class
I bind the ItemsControl to an ObservableCollection of ViewModels, but the ViewModels do not appear. I have tried using an ItemsTemplate to create a textbox setting the Text={Binding Path=CurrentItem.Year} to no avail.
XAML:
<Page x:Class="ItemPage"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
Name="ItemPage"
Title="ItemPage" Loaded="Page_Loaded_1" Margin="10">
<Grid>
<ItemsControl ItemsSource="{Binding Path=ItemCollection}" />
</Grid>
</Page>
Here is the code-behind:
Imports OClient = Oracle.DataAccess.Client
Imports System.Collections.ObjectModel
Imports System.Data
Class ItemPage
Private ds As DataSet
Private itemsTable As DataTable
Public Property ItemsCollection As ObservableCollection(Of ItemViewModel)
Private Sub Page_Loaded_1(sender As Object, e As RoutedEventArgs)
Dim itemsQry = "select item_year from items order by item_year desc"
Dim queryCmd As New OClient.OracleCommand(itemsQry, O.con)
Dim adapter As New OClient.OracleDataAdapter(queryCmd)
ds = New DataSet
adapter.Fill(ds, "items")
itemsTable = ds.Tables("items")
ItemsCollection = New ObservableCollection(Of ItemViewModel)
For Each r As DataRow In surveys.Rows
Dim newItem As New ItemViewModel
newItem.CurrentItem.Year = r.Item("ITEM_YEAR").ToString
Next
Me.DataContext = Me
End Sub
End Class
I am having a very hard time figuring out where my app is falling apart. Is it in my implementation of the ViewModels? Am I not binding the data correctly? Do I need to do something different with my ObservableCollection?
Thanks for helping a newbie.

You iterate over the elements of surveys.Rows and create a new ItemViewModel for each one, but you never add them to ItemsCollection, by ItemsCollection.Add(newItem):
For Each r As DataRow In surveys.Rows
Dim newItem As New ItemViewModel
newItem.CurrentItem.Year = r.Item("ITEM_YEAR").ToString
ItemsCollection.Add(newItem)
Next
You're also using a wrong Path for the ItemsSource Binding. It must be ItemsCollection instead of ItemCollection.
Besides that, instead of overriding ToString() you should declare a DataTemplate:
<ItemsControl ItemsSource="{Binding ItemsCollection}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock
Text="{Binding CurrentItem.Year, StringFormat={}{0} Item ViewModel}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

Related

Can't declare the viewmodel in XAML for this code

I've reviewed as many 'how to declare the viewmodel in XAML' posts I can and still can't figure this out. I'm using the simple program below to learn the basics regarding binding and this code works. When I click to insert(add) items, the listbox automatically reflects that change, as well as when I clear the list.
See my question(s) after the code.
Model
Namespace MVVM3
Public Class MyListItem
Public Property MyListItemID() As Integer
Public Property Name() As String
End Class
End Namespace
ViewModel
Namespace MVVM3
Public Class ViewModel
Implements INotifyPropertyChanged
Public Property allIdeas() As New ObservableCollection(Of MyListItem)
Public Sub New()
For index = 0 To 9
Dim anItem As New MyListItem
anItem.Name = "Idea " & index
allIdeas.Add(anItem)
Next
End Sub
Public Sub InsertAnItem()
Dim anItem As New MyListItem
anItem.Name = "Item " & allIdeas.Count()
allIdeas.Add(anItem)
NotifyPropertyChanged("allIdeas")
End Sub
Public Sub ClearStoredList()
allIdeas.Clear()
NotifyPropertyChanged("allIdeas")
End Sub
Public Event PropertyChanged As PropertyChangedEventHandler _
Implements INotifyPropertyChanged.PropertyChanged
Public Sub NotifyPropertyChanged(ByVal propertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
End Class
End Namespace
View
<Window x:Class="MVVM3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:mvvm3"
Title="MainWindow" SizeToContent="WidthAndHeight" WindowStartupLocation="CenterScreen"
>
<StackPanel Grid.Column="1">
<Button Margin="25" Content="Insert an item" Click="InsertAnItem_Click"/>
<Button Margin="25" Content="Clear stored list" Click="ClearStoredList_Click"/>
<ListBox Name="listBox3" ItemsSource="{Binding allIdeas}" DisplayMemberPath="Name" Height="100">
</ListBox>
</StackPanel>
</Window>
Code Behind
Namespace MVVM3
Partial Class MainWindow
Private vm = New ViewModel
Sub New()
InitializeComponent()
DataContext = vm
End Sub
Private Sub InsertAnItem_Click(sender As Object, e As RoutedEventArgs)
vm.InsertAnItem()
End Sub
Private Sub ClearStoredList_Click(sender As Object, e As RoutedEventArgs)
vm.ClearStoredList()
End Sub
End Class
End Namespace
I want to move the ViewModel declaration into XAML to eliminate code behind. If I comment out DataContext = vm, no matter what method I've followed from various posts, the binding no longer updates the listbox.
The following changes result in the listbox showing the initial assignment that takes place in ViewModel.New, but after that no changes are reflected:
<Window x:Class="MVVM3.MainWindow"
...
xmlns:local="clr-namespace:mvvm3"
xmlns:vm="clr-namespace:mvvm3.MVVM3"
...
>
<Window.DataContext>
<vm:ViewModel/>
</Window.DataContext>
What am I missing? Is it a namespace problem?
I'm hoping to move on to Commands and ViewModel locators, but I don't see how I can do that until I understand this.
When you declare an instance of the ViewModel class in XAML, you may access it in code behind by casting the value of the DataContext property to the ViewModel type:
Partial Class MainWindow
Sub New()
InitializeComponent()
End Sub
Private Sub InsertAnItem_Click(sender As Object, e As RoutedEventArgs)
CType(DataContext, ViewModel).InsertAnItem()
End Sub
Private Sub ClearStoredList_Click(sender As Object, e As RoutedEventArgs)
CType(DataContext, ViewModel).ClearStoredList()
End Sub
End Class

Why My MVVM does not work when I change data on the fly?

My xaml:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestMetroChartsVB"
xmlns:chart="clr-namespace:GravityApps.Mandelkow.MetroCharts;assembly=GravityApps.Mandelkow.MetroCharts"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:TestPageViewModel/>
</Window.DataContext>
<Grid>
<StackPanel>
<chart:ClusteredBarChart ChartTitle="Example1 " ChartSubTitle="Test1">
<chart:ClusteredBarChart.Series>
<chart:ChartSeries SeriesTitle="Errors" ItemsSource="{Binding Errors}"
DisplayMember="Category" ValueMember="Number"/>
</chart:ClusteredBarChart.Series>
</chart:ClusteredBarChart>
<Button x:Name="btnTest" Height="31" Margin="0,0,405,0" Content="Change Data"/>
</StackPanel>
</Grid>
</Window>
My view:
Imports GravityApps.Mandelkow.MetroCharts Class MainWindow
Private Sub btnTest_Click(sender As Object, e As RoutedEventArgs) Handles btnTest.Click
Dim test As New TestPageViewModel
test.changeData()
End Sub End Class
My viewModel:
Imports System.Collections.ObjectModel
Public Class TestPageViewModel
Public Property Errors() As ObservableCollection(Of TestClass)
Get
Return m_Errors
End Get
Private Set
m_Errors = Value
End Set
End Property
Private m_Errors As ObservableCollection(Of TestClass)
Public Sub New()
Errors = New ObservableCollection(Of TestClass)
Errors.Add(New TestClass("Data1", 5))
Errors.Add(New TestClass("Data2", 10))
Errors.Add(New TestClass("Data5", 15))
End Sub
Public Sub changeData()
Errors.Clear()
Errors.Add(New TestClass("DAta9", 10))
End Sub
End Class
My Model:
Public Class TestClass
Public Property Category As String
Get
Return m_Category
End Get
Set(value As String)
m_Category = value
End Set
End Property
Private m_Category As String
Public Property Number As Integer
Get
Return m_Number
End Get
Set(value As Integer)
m_Number = value
End Set
End Property
Private m_Number As Integer
Public Sub New(category As String, number As Integer)
Me.Category = category
Me.Number = number
End Sub End Class
It displays data initially like this:
But when I click my "change data" button data does not change?
What's the problem?
You are changing the wrong data:
Private Sub btnTest_Click(sender As Object, e As RoutedEventArgs) Handles btnTest.Click
Dim test As New TestPageViewModel
test.changeData()
End Sub
In your button's Click event handler, you create a new view model object and call the changeData() on it. But that's not the view model object the view is using, nor do you do anything to make it the view model object the view is using.
The best thing would be to just get the actual view model and change that one:
Private Sub btnTest_Click(sender As Object, e As RoutedEventArgs) Handles btnTest.Click
Dim view As Button = CType(sender, Button)
Dim test As TestPageViewModel = CType(view.DataContext, TestPageViewModel)
test.changeData()
End Sub
The above assumes that the Button sending the Click event has inherited the MainWindow's data context.

WPF ComboBox DataBinding vb.net

I am trying to populate a combobox using databinding.Below are the snippets of my code.
XAML:
<ComboBox Name="cmbClientName" ItemsSource="{Binding Path=blClientList}" DisplayMemberPath="Name" SelectedValuePath="Name" SelectedValue="{Binding Path=blClientList}"/>
Code behind 'MainWindow':
Private Sub MainWindow_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
Dim blClientList As ObservableCollection(Of String) = New ObservableCollection(Of String)
Dim objClientRepository As PET_ClientRepository = New PET_ClientRepository
For Each objClient As PET_Client In objClientRepository.GetAll()
blClientList.Add(objClient.Name)
Next
Me.cmbClientName.ItemsSource = blClientList
End Sub
Class1.vb:
Public Property Name As String
Get
Return _Name
End Get
Set(value As String)
_Name = value
End Set
End Property
I would like to see the name of the client in my combobox. The code above does not return anything and combobox is blank.I know the problem lies in binding but can not figure out what I am doing wrong.
Here's an example that may help you achieve your desired result:
Re: your comment, binding is good, you just need to set context
WPF
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication4VB"
Title="MainWindow" Height="100" Width="293">
<Window.DataContext>
<local:Vm />
</Window.DataContext>
<Grid>
<ComboBox ItemsSource="{Binding Collection}" DisplayMemberPath="Name" />
</Grid>
</Window>
important things being that you add your local namespace:
xmlns:local="clr-namespace:WpfApplication4VB"
and that you set DataContext
Classes
Public Class Vm
Public Property Collection As System.Collections.ObjectModel.ObservableCollection(Of Thing)
Public Sub New()
Collection = New ObjectModel.ObservableCollection(Of Thing)()
Collection.Add(New Thing("Test1"))
Collection.Add(New Thing("Test2"))
Collection.Add(New Thing("Test3"))
End Sub
End Class
Public Class Thing
Public Property Name As String
Public Sub New(Name As String)
Me.Name = Name
End Sub
End Class
Thank you everyone for your help.
I went ahead with populating the combobox in the code behind in stead of binding it to a datasource. I didn't even have to pass "ItemSource" in XAML. I did however pass ItemSource in Mainwindow.vb which did the trick for me.
Below are the snippets of code that are working:
Code behind MainWindow.vb
Private Sub MainWindow_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
Me.Username.Text = System.Security.Principal.WindowsIdentity.GetCurrent().Name
Dim blClientList As ObservableCollection(Of String) = New ObservableCollection(Of String)
Dim objClientRepository As PET_ClientRepository = New PET_ClientRepository
For Each objClient As PET_Client In objClientRepository.GetAll()
blClientList.Add(objClient.Name)
Next
Me.cmbClientName.ItemsSource = blClientList
XAML:
<ComboBox Name="cmbClientName" IsReadOnly="True" PresentationTraceSources.TraceLevel="High"/>
Tracelevel isn't doing anything but is just there to provide debug tracing support.
Class1.vb
Public Property Name As String
Get
Return _Name
End Get
Set(value As String)
_Name = value
End Set
End Property
Replace this:
Me.cmbClientName.ItemsSource = blClientList
with this:
Me.DataContext = blClientList
and correct XAML as:
<ComboBox Name="cmbClientName" ItemsSource="{Binding}"
DisplayMemberPath="." SelectedValuePath="."/>

Accessing ListView within an ItemTemplate

I have an ItemsControl that is bound to a list of Objects.
I have an ItemTemplate set for the ItemsControl... and within the ItemTemplate I have a ListView, which I want to bind to another list of Objects.
How do I gain access to the ListView within the ItemsControl.ItemTemplate so that I can set it's DataContext to the second list of objects?
Here is an example of what I have:
<ItemsControl x:Name="DealerShips" ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Expander Header="{Binding Name}" Foreground="White">
<ListView x:Name="CarTypes" ItemsSource="{Binding}">
<ListView.ItemTemplate>
<CheckBox Content="{Binding Path=Name}" IsChecked="{Binding Path=InStock}"></CheckBox>
</ListView.ItemTemplate>
</ListView>
</Expander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
In my VB.NET code I can refer to my "DealerShips" ItemsControl to set the DataContext to a List(Of DealerShip) Objects. I can't figure out how to gain access to the "CarTypes" ListView so that I can set it's DataContext.
Public Class CarsWindow
Private _listOfDealerShips As ObservableCollection(Of DealerShip)
Private _listOfInventory As ObservableCollection(Of Car)
Private Sub Window_Loaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles MyBase.Loaded
_listOfDealerShips = New ObservableCollection(Of DealerShip)
listOfDealerShips.Add(New DealerShip("A"))
listOfDealerShips.Add(New DealerShip("B"))
listOfDealerShips.Add(New DealerShip("C"))
_listOfInventory = New ObservableCollection(Of Cars)
listOfDealerShips.Add(New Car("TypeX",True))
listOfDealerShips.Add(New Car("TypeY",False))
listOfDealerShips.Add(New Car("TypeZ",True))
DealerShips.DataContext = _listOfDealerShips
' I cannot access CarTypes....
'CarTypes.DataContext = listOfInventory
End Sub
Private Class DealerShip
Public Property Name As String
Public Sub New
End Sub
Public Sub New(ByVal name As String)
Me.Name = name
End Sub
End Class
Private Class Car
Public Property Name As String
Public Property InStock As Boolean
Public Sub New
End Sub
Public Sub New(ByVal name As String, ByVal isInStock As Boolean)
Me.Name = name
Me.InStock = isInStock
End Sub
End Class
End Class
Thank you for your help!
-Frinny
Can you setup a viewmodel that contains your lists and make them public, set the cars window's DataContext to an instance of the viewmodel (maybe via DI), then change your listview in xaml to look like this:
<ListView x:Name="CarTypes" ItemsSource="{Binding DataContext.ListOfInventory, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}">
I ended up creating a class that had both lists within it.
Like this:
Public Class DealershipDetails
Public Property ListOfDealerShips As ObservableCollection(Of DealerShip)
Public Property ListOfInventory As ObservableCollection(Of Car)
Public Sub New()
ListOfDealerShips = New ObservableCollection(Of DealerShip)
ListOfDealerShips.Add(New DealerShip("A"))
ListOfDealerShips.Add(New DealerShip("B"))
ListOfDealerShips.Add(New DealerShip("C"))
ListOfInventory = New ObservableCollection(Of Cars)
ListOfInventory.Add(New Car("TypeX",True))
ListOfInventory.Add(New Car("TypeY",False))
ListOfInventory.Add(New Car("TypeZ",True))
End Sub
End Class
That way I could bind the ItemsControl to an instance of the DealershipDetails class and selectively bind the controls within the ItemsControl to the appropriate lists.
While this approach works, my original question remains unanswered.
I do not know how to access a child control.
-Frinny

WPF ComboBox binding not working as expected

I want my WPF ComboBox's ItemsSource property to be bound to MyListObject's MyList property. The problem is that when I update the MyList property in code, the WPF ComboBox is not reflecting the update. I am raising the PropertyChanged event after I perform the update, and I thought WPF was supposed to automatically respond by updating the UI. Am I missing something?
Here's the CLR object:
Imports System.ComponentModel
Public Class MyListObject
Implements INotifyPropertyChanged
Private _mylist As New List(Of String)
Public Sub New()
_mylist.Add("Joe")
_mylist.Add("Steve")
End Sub
Public Property MyList() As List(Of String)
Get
Return _mylist
End Get
Set(ByVal value As List(Of String))
_mylist = value
End Set
End Property
Public Sub AddName(ByVal name As String)
_mylist.Add(name)
NotifyPropertyChanged("MyList")
End Sub
Private Sub NotifyPropertyChanged(ByVal info As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(info))
End Sub
Public Event PropertyChanged(ByVal sender As Object, _
ByVal e As System.ComponentModel.PropertyChangedEventArgs) _
Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
End Class
Here is the XAML:
<Window x:Class="Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300"
xmlns:local="clr-namespace:WpfApplication1"
>
<Window.Resources>
<ObjectDataProvider x:Key="MyListObject" ObjectType="{x:Type local:MyListObject}"/>
</Window.Resources>
<Grid>
<ComboBox Height="23"
Margin="24,91,53,0"
Name="ComboBox1"
VerticalAlignment="Top"
ItemsSource="{Binding Path=MyList, Source={StaticResource MyListObject}}"
/>
<TextBox Height="23"
Margin="24,43,134,0"
Name="TextBox1"
VerticalAlignment="Top" />
<Button Height="23"
HorizontalAlignment="Right"
Margin="0,43,53,0"
Name="btn_AddName"
VerticalAlignment="Top"
Width="75">Add</Button>
</Grid>
</Window>
And here's the simple code-behind:
Class Window1
Private obj As New MyListObject
Private Sub btn_AddName_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) _
Handles btn_AddName.Click
obj.AddName(TextBox1.Text)
End Sub
End Class
Thanks!
You are binding to a list of strings. That list class does not implement Inotifyproperty. You should use an observablecollection instead.
I also notice in your code behind you declare
Private obj As New MyListObject
This is not the static resource you bound the combo box to. So your add call would not be reflected in your view.
The ObservableCollection is most likely the solution, but if it still gives you grief, you can directly access your static resource by calling the following code after your list gets updated:
DirectCast(Me.FindResource("MyListObject"), ObjectDataProvider).Source = _myList
Try using a BindingList(Of T) instead of a List(Of T).
Edit: I am new to WPF and it does look like BindingList isn't a complete solution to your problem, but it might be a step in the right direction. I was able to test the MyListObject converted to BindingList in WinForm and the ListChanged event was raised to the ComboBox which then updated its list.
I found this (possible) solution to wrap your class in an ObservableCollection that might help you solve your problem
Enabling WPF Magic Using WCF - Part 1
This is the code to update your object to a BindingList. Combine your code with the code from that resource and you should be good to go.
Public Class MyListObject
...
'Private _mylist As New List(Of String)
Private _mylist As New BindingList(Of String)
...
'Public Property MyList() As List(Of String)
' Get
' Return _mylist
' End Get
' Set(ByVal value As List(Of String))
' _mylist = value
' End Set
'End Property
Public Property MyList() As BindingList(Of String)
Get
Return _mylist
End Get
Set(ByVal value As BindingList(Of String))
_mylist = value
End Set
End Property
...
End Class

Resources