Accessing ListViewItems Within a Custom INotifyPropertyChanged Class - c#

I have a ListView with a bound ItemsSource and a GridViewColumns to display a given items property. Here is a snippet of the important bits:
<ListView Name="listView1" ItemsSource="{Binding Path=listItemCollection}" DataContext="{Binding ListItem}">
<ListView.View>
<GridView x:Name="gridViewMain">
<GridViewColumn x:Name="listViewItemNumberColumn" Width="50">
<GridViewColumnHeader>#</GridViewColumnHeader>
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock x:Name="idTextBlock" KeyboardNavigation.IsTabStop="False" HorizontalAlignment="Center" Text="{Binding Path=Id}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn x:Name="listViewItemNameColumn" Width="500">
<GridViewColumnHeader>Name</GridViewColumnHeader>
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox x:Name="nameTextBox" TextChanged="nameTextBox_TextChanged" Text="{Binding Path=ItemName, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
...
I want to be able to focus on a specific field/textbox within a user-selected ListViewItem. I am able to find the ListViewItem from a given index using this code:
listView1.ItemContainerGenerator.ContainerFromIndex(i);
All is well and good so far. That is until I want to actually access the content within the ListViewItem. Instead of ListViewItem.Content returning a group of controls which I am expecting, it returns my custom class ListItem. Here is a snippet of what that class looks like:
public class ListItem : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
public TextBlock idTextBlock;
public GridView gridViewMain;
public bool Selected { get; set; }
public string ItemName { get; set; }
I am completely lost in how I would for example focus onto the nameTextBox text box control within a ListViewItem.
Is it possible to somehow link the controls such as the nameTextBox and idTextBlock to my custom ListItem class?

There is a property in ListView SelectedItem. You need to bind this to.
In your view
<ListView Name="listView1" ItemsSource="{Binding Path=listItemCollection}" SelectedItem="{Binding SelectedListItem}" DataContext="{Binding ListItem}">
And in your class where you have listItemCollection, add a SelectedListItem variable.
public ListViewItem SelectedListViewItem {get; set;}
EDIT:
Remove ListView. Use DataGridOnly.
XAML:
<DataGrid ItemsSource="{Binding MyItems}" SelectedItem="{Binding SelectedItem}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Name}" Header="Name" Width="175" />
<DataGridTextColumn Binding="{Binding Path}" Header="Path" Width="75" />
</DataGrid.Columns>
</DataGrid>
ViewModel:
ObservableCollection<MyItem> MyItems {get; set;}
MyItem SelectedItem {get; set;}
public class MyItem : INotifyPropertyChanged
{
public string Name {get; set;}
public string Path {get; set;}
}

Related

How can I update the DataTemplate of a DataGrid.RowDetailsTemplate

Im my viewmodel I have a list of my custom objects of type IPC.Device bound to a property defined as
private ObservableCollection<IPC.Device> _devices;
public ObservableCollection<IPC.Device> Devices
{
get
{
return _devices;
}
set
{
_devices = value;
RaisePropertyChangedEvent ("Devices");
}
}
I use the ObservableCollection to populate a DataGrid, that I create with the following XAML (unnecessary parts are not shown)
<DataGrid x:Name="MainGrid" ItemsSource="{Binding Path=Devices}" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False">
<DataGrid.Columns>
....
<DataGridTemplateColumn Header="{Binding Source={x:Static p:Resources.device_status},
Converter={StaticResource CapitalizeFirstLetterConverter}}"
Width="*" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=deviceStatus, Converter={StaticResource DeviceStatusToStringConverter}, Mode=OneWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
....
</DataGrid.Columns>
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<Views:CameraLoginControl Visibility="{Binding Path=deviceStatus, Converter={StaticResource DeviceStatusUnauthorizedConverter}, Mode=OneWay}" />
<Views:TestSelectionControl Visibility="{Binding Path=deviceStatus, Converter={StaticResource DeviceStatusOnlineConverter}, Mode=TwoWay}" />
</StackPanel>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid>
When I update the ObservableCollection byt replacing an item with its updated copy I can see that the Datagrid is correctly updated, the row is updated and the DeviceStatusToStringConverter is fired. Unfortunately, the DeviceStatusUnauthorizedConverter and DeviceStatusOnlineConverter are never fired again, so the RowDetailsTemplate is still the one of the previous item.
What am I doing wrong?
Update
The IPC.Device is, stripped of unnecessary fields:
public class Device
{
[DataMember]
public DeviceStatus deviceStatus { get; set; }
...
}
You see [DataMember] beacsue I use this structure in many places, also for IPC. I have other Propertied marked [DataMember], thus I would exclude this as the reason why this happens. DeviceStatus is an enum.
There are several possibilities to do it.
You can implement INotifyPropertyChanged for the
Device.deviceStatus, then you don't need to replace an object in
ObservableCollection, just modify the property.
You can replace the whole ObservableCollection Devices = new
ObservableCollection...
You can remove and insert an element:
Devices.RemoveAt(i);
Devices.Insert(i, new Device());

How do I update the CellTemplate DataTemplate control when the CellEditingTemplate DataTemplate control changes its bound value?

I have a Template Column in my DataGrid that looks like this:
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Item}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox
DisplayMemberPath="Item"
Header="Item"
ItemsSource="{Binding Data.AssetDescriptions, Source={StaticResource proxy}}"
SelectedValueBinding="{Binding AssetDescriptionID}"
SelectedValuePath="AssetDescriptionID" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
The ViewModel has a public property containing the Asset Descriptions:
public IEnumerable<AssetDescription> AssetDescriptions { get; set; }
Where AssetDescription is essentially:
public class AssetDescription
{
public int AssetDescriptionID { get; set; }
public string Item { get; set; } // Description
}
The DataGrid itself is bound to an ObservableCollection<Asset> Assets property, where Asset contains both AssetDescriptionID and Item (description). To accomplish that, I join the Assets table to the AssetDescriptions table, like so:
var assets = _conn.Query<Asset>(
#"SELECT A.AssetDescriptionID, D.Item
FROM Assets A
JOIN AssetDescriptions D
ON D.AssetDescriptionID = A.AssetDescriptionID");
Assets = new ObservableCollection<Asset>(assets);
This all works perfectly, except that the TextBlock in the CellTemplate DataTemplate does not get updated to the new description when a new value is selected in the ComboBox.
How do I accomplish that?
The problem is that you only bind to the AssetDescriptionID-Property of your Asset.
Item will never be touched (which your CellTemplate binds to).
Option 1:
Try using a DataGridComboBoxColumn instead of DataGridTemplateColumn
Item (on Asset) is then no longer needed
<DataGridComboBoxColumn
DisplayMemberPath="Item"
Header="Item"
ItemsSource="{Binding Data.AssetDescriptions, Source={StaticResource proxy}}"
SelectedValueBinding="{Binding AssetDescriptionID}"
SelectedValuePath="AssetDescriptionID">
</DataGridComboBoxColumn>
Option 2: If you really need the Item/description on your Asset
Easiest solution will be to bind the whole object(AssetDescription).
Change your Asset to this
class Asset
{
...
public AssetDescription AssetDescription {get;set;}
...
}
And your CellEditingTemplate to this
<DataTemplate>
<ComboBox
DisplayMemberPath="Item"
ItemsSource="{Binding Data.AssetDescriptions, Source={StaticResource proxy}}"
SelectedItem="{Binding AssetDescription }" />
</DataTemplate>
And CellTemplate to this
<TextBlock Text="{Binding AssetDescription.Item}" />
Edit:
You can also use a DataGridComboBoxColumn for Option 2
<DataGridComboBoxColumn
DisplayMemberPath="Item"
Header="Item"
ItemsSource="{Binding Data.AssetDescriptions, Source={StaticResource proxy}}"
SelectedItemBinding="{Binding AssetDescription}">
</DataGridComboBoxColumn>

DataGrid RowValidationRule won't be called on combobox change

I wanna validate my DataGrid with an RowValidationRule, but the ValidationRule will not be called if I select another item within the combobox.
The Validation Rule will only be called if I change the text within the DatagridTextColumn.
<DataGrid AutoGenerateColumns="False"
ItemsSource="{Binding SelectedFiles, UpdateSourceTrigger=PropertyChanged}"
SelectedIndex="{Binding SelectedDataGridRow, UpdateSourceTrigger=PropertyChanged}"
CellEditEnding="SelectedFilesDataGrid_CellEditEnding">
<DataGrid.RowValidationRules>
<valid:SaveComponentValidationRule ValidationStep="UpdatedValue"/>
</DataGrid.RowValidationRules>
<DataGrid.Resources>
<DataTemplate x:Key="comboTemplate">
<ComboBox IsSynchronizedWithCurrentItem="False"
ItemsSource="{Binding DataContext.FileDataTypes, ValidatesOnDataErrors=True,
RelativeSource={RelativeSource AncestorType=local:SaveComponentView},UpdateSourceTrigger=PropertyChanged}"
SelectedValue="{Binding RelatedFileType, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding IsEditing, RelativeSource={RelativeSource AncestorType=DataGridCell}}"/>
</DataTemplate>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Header="File Name" Binding="{Binding Path=FileName, UpdateSourceTrigger=PropertyChanged}"/>
<DataGridTemplateColumn Header="Data Type"
CellTemplate="{StaticResource comboTemplate}"
CellEditingTemplate="{StaticResource comboTemplate}" />
</DataGrid.Columns>
</DataGrid>
The binding SelectedFiles of the datagrid is an ObservableCollection, which contains the objects of the posted class below.
public class SelectedFileModel
{
public FileType RelatedFileType {get; set;}
public string FileName {get; set;}
public string FilePath {get; set;}
}
The combobox contains a enum of the type FileType.
And the CellEditEnding method in my View.
private void SelectedFilesDataGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
e.EditingElement.IsEnabled = false;
}

Combobox(inside a DataGrid) Binding to Observable List not loading

I have a ComboBox column inside a DataGrid. I need it to display values based on another WPF control, not in DataGrid. So, the values of ComboBox should change according to that control. I have created an ObservableCollection and binded it to the ComboBox but it does not display any values. For displaying dynamically, I have added DropOpenOpened event. But the ComboBox does not display any value. The list that populates the ComboBox is getting updated but its not displaying anything.
Below is the xaml code.
The DataGrid is bound to another List, whose values I fetch from DB.
<DataGrid x:Name="grid1" AutoGenerateColumns="False" Grid.Row="1" Grid.RowSpan="2" Grid.Column="1" Grid.ColumnSpan="2"
AlternatingRowBackground="Azure" AlternationCount="2" CanUserReorderColumns="True" CanUserResizeRows="True" CanUserSortColumns="True"
DataContext="attr">
<DataGrid.Columns>
<DataGridTextColumn Width="*" Binding="{Binding modify_user}" Header="Modified By" IsReadOnly="True"></DataGridTextColumn>
<DataGridTextColumn Width="*" Binding="{Binding modify_date}" Header="Modified Date" IsReadOnly="True"></DataGridTextColumn>
<DataGridTemplateColumn Header="Source" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding source_value}"></TextBlock>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox x:Name="combo_source" ItemsSource="{Binding Path=sourceComboDropdown}"
DisplayMemberPath="desc" SelectedValuePath="id"
SelectedItem="{Binding source_value}"
SelectedValue="{Binding Path=source_value,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
DropDownOpened="combo_source_DropDownOpened"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Target value" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding target_value}"></TextBlock>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding targetComboDropdown}"
DisplayMemberPath="desc" SelectedValuePath="id"
SelectedItem="{Binding target_value}"
SelectedValue="{Binding Path=target_value,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
DropDownOpened="ComboBox_DropDownOpened"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
The class of observablecollection is:
public static ObservableCollection<Source> sourceComboDropdown =
new ObservableCollection<Source>();
public static ObservableCollection<Source> targetComboDropdown =
new ObservableCollection<Source>();
public class Source
{
public string id { get; set; }
public string desc { get; set; }
}
Where desc is the DisplayMember value and id is SelectedValue.
The class for DataGrid list is:
public static ObservableCollection<Attribute_Param> attr =
new ObservableCollection<Attribute_Param>();
public class Attribute_Param
{
public string source_value { get; set; }
public string target_value { get; set; }
public string modify_user { get; set; }
public DateTime modify_date { get; set; }
}
I have tried adding static resource. But since I need to dynamically update the values, I couldn't figure out a work around to use it.
I think I am missing something really small but cant figure out what.
I think it is unable to find the list sourceComboDropdown since the dataContext of the DataGrid is attr which does not CONTAIN sourceCombo drop down.
I would set my datagrid datacontext to the whole viewmodel. i.e DataGrid.DataContext=model in the constructor of the code behind.
Then I can bind the datagrid to attr using : ItemsSource="{Binding attr}" inside the DataGrid tag . Assuming attr is immediately in the VieWModel model. That way it should be able to detect the combo box items soruce which is now in the data context i.e model contains sourceDropDown .
If that doesnt work , try this :
<DataGridTemplateColumn >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate >
<ComboBox Loaded="LoadItemsSource"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
private void LoadItemsSource(object sender, RoutedEventArgs e)
{
ComboBox comboBox = sender as ComboBox;
comboBox.ItemsSource=model.sourceDropDown;
}

WPF Listview databinding to generic List, dynamically filled by WCF service

In WPF app I have a WCF service which dynamically fills a generic List object from a backend database.
How in this case (List created in runtime), I could bind List items to a ListView object items?
It is the Data contract for my Web service:
....
[DataContract]
public class MeetList
{
[DataMember]
public string MeetDate;
[DataMember]
public string MeetTime;
[DataMember]
public string MeetDescr;
.....
static internal List<MeetList> LoadMeetings(string dynamicsNavXml)
{
...// Loads XML stream into the WCF type
}
Here in this event handler I read the WCF service and Loop through a List object:
private void AllMeetings()
{
Customer_ServiceClient service = new Customer_ServiceClient();
foreach (MeetList meet in service.ReadMeetList())
{
?????? = meet.MeetDate; // it's here that I bumped into a problem
?????? = meet.MeetTime; //
?????? = meet.MeetDescr;//
}
}
My Listview XAML:
<Grid>
<ListView Height="100" Width="434" Margin="0,22,0,0" Name="lvItems" ItemsSource="{Binding}" IsSynchronizedWithCurrentItem="True" SelectionMode="Single">
<ListView.View>
<GridView>
<GridViewColumn Header="Date" Width="100" HeaderTemplate="{StaticResource DateHeader}" CellTemplate="{DynamicResource DateCell}"/>
<GridViewColumn Header="Time" Width="100" HeaderTemplate="{StaticResource TimeHeader}" CellTemplate="{DynamicResource TimeCell}"/>
<GridViewColumn Header="Description" Width="200" HeaderTemplate="{StaticResource DescriptionHeader}" CellTemplate="{DynamicResource DescriptionCell}"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
And data templates for this ListView:
<Window.Resources>
<DataTemplate x:Key="DateHeader">
<StackPanel Orientation="Horizontal">
<TextBlock Margin="10,0,0,0" Text="Date" VerticalAlignment="Center" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="DateCell" DataType="Profile">
<StackPanel Orientation="Horizontal">
<TextBlock>
<TextBlock.Text>
<Binding Path="MeetDate" />
</TextBlock.Text>
</TextBlock>
</StackPanel>
</DataTemplate>
......
How in this case (List created in runtime), I could bind my generic List items to a ListView object items?
I tried to use lvItems.ItemsSource = profiles; , but it doesn't work in my event handler
List doesn't have behaviour to notify that items count is changed. You should use a list with support INotifyCollectionChanged.. for example: ObservableCollection<T>. ObservableCollection<T> will inform your lvItems that items count is changed and it will be properly display.
Using intermediate ObservableCollection:
ObservableCollection<Meets> _MeetCollection =
new ObservableCollection<Meets>();
public ObservableCollection<Meets> MeetCollection
{ get { return _MeetCollection; } }
public class Meets
{
public string Date { get; set; }
public string Time { get; set; }
public string Description { get; set; }
}
In the event handler loop we put:
private void AllMeetings()
{
Customer_ServiceClient service = new Customer_ServiceClient();
_MeetCollection.Clear();
foreach (MeetList meet in service.ReadMeetList())
{
_MeetCollection.Add(new Meets
{
Date = meet.MeetDate,
Time = meet.MeetTime,
Description = meet.MeetDescr
});
}
}
And XAML binding is changed to:
<Grid>
<ListView Height="100" Width="434" Margin="0,22,0,0" Name="lvItems" ItemsSource="{Binding MeetCollection}">
<ListView.View>
<GridView>
<GridViewColumn Header="Date" Width="100" DisplayMemberBinding="{Binding Date}"/>
<GridViewColumn Header="Time" Width="100" DisplayMemberBinding="{Binding Time}"/>
<GridViewColumn Header="Description" Width="200" DisplayMemberBinding="{Binding Description}"/>
</GridView>
</ListView.View>
</ListView>
</Grid>

Categories