ICollectionView Sorting issue in ListView - wpf

I saw ICollectionView being introduced with WPF to handle situations when you need sorting and filtering enabled. I even saw few articles which sort items, but my main concern is why my approach is failing. Lets see my code :
<ListView ItemsSource="{Binding}" x:Name="lvItems" GridViewColumnHeader.Click="ListView_Click">
<ListView.View>
<GridView AllowsColumnReorder="True">
<GridViewColumn Header="Id" DisplayMemberBinding="{Binding Id}" />
<GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" />
<GridViewColumn Header="Developer">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Developer}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Salary">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Salary}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
In codebehind, when the Item is clicked I am doing like this :
ICollectionView Source { get; set; }
private void ListView_Click(object sender, RoutedEventArgs e)
{
GridViewColumnHeader currentHeader = e.OriginalSource as GridViewColumnHeader;
if(currentHeader != null && currentHeader.Role != GridViewColumnHeaderRole.Padding)
{
//using (this.Source.DeferRefresh())
//{
SortDescription currentPropertySort = this.Source.SortDescriptions.FirstOrDefault<SortDescription>(item => item.PropertyName.Equals(currentHeader.Column.Header.ToString()));
if (currentPropertySort != null)
{
if (currentPropertySort.Direction == ListSortDirection.Ascending)
currentPropertySort.Direction = ListSortDirection.Descending;
else
currentPropertySort.Direction = ListSortDirection.Ascending;
}
else
this.Source.SortDescriptions.Add(new SortDescription(currentHeader.Column.Header.ToString(), ListSortDirection.Ascending));
//}
this.Source.Refresh();
this.lvItems.DataContext = this.Source;
this.lvItems.UpdateLayout();
}
}
So whenever the header for the ListBox is clicked, the item need to be sorted. I am holding the collection using a property called Source and then using it by calling lvItems.DataContext = this.Source. But the code does not seem to be working.

Here's an updated version of your ListView_Click method that somewhat works. I'm not sure exactly what sorting behavior you were looking for but the version below "stacks up" a set of sort descriptions, making the last clicked column as the "primary sort description". I hope this makes sense and I hope the code below helps. =)
private void ListView_Click(object sender, RoutedEventArgs e)
{
GridViewColumnHeader currentHeader = e.OriginalSource as GridViewColumnHeader;
if(currentHeader != null && currentHeader.Role != GridViewColumnHeaderRole.Padding)
{
if (this.Source.SortDescriptions
.Count((item) => item.PropertyName.Equals(currentHeader.Column.Header.ToString())) > 0)
{
SortDescription currentPropertySort = this.Source
.SortDescriptions
.First<SortDescription>(item => item.PropertyName.Equals(currentHeader.Column.Header.ToString()));
//Toggle sort direction.
ListSortDirection direction =
(currentPropertySort.Direction == ListSortDirection.Ascending)?
ListSortDirection.Descending : ListSortDirection.Ascending;
//Remove existing sort
this.Source.SortDescriptions.Remove(currentPropertySort);
this.Source.SortDescriptions.Insert(0, new SortDescription(currentHeader.Column.Header.ToString(), direction));
}
else
{
this.Source.SortDescriptions.Insert(0, new SortDescription(currentHeader.Column.Header.ToString(), ListSortDirection.Ascending));
}
this.Source.Refresh();
}
}
EDIT:
By the way, one of the problems in your code above is your call to "FirstOrDefault" to query an existing SortDescription. See, SortDescription is a struct, which is non-nullable so the call to FirstOrDefault will never be null and will always return an instance. Therefore, the "else-statement" in your code above will never get called.

Related

WPF grid not refreshing on second process

I am working with WPF + some custom templates/extensions, MVVM architecture. I have a grid that displays records. If user selects one or more rows and clicks 'process' the records will be marked as inactive in the database. Once this completes the grid refreshes and the inactivated records no longer appear. However if the user selects another records and clicks 'process' the record is marked inactive in the database but the grid does not refresh. I have it set up almost exactly the same on another page and it works. When I debug the NotifyPropertyChanged event is triggered both times. Why is the message not getting to the grid the second time (or any time after that)?
here is my ViewModel and View (drastically cut down but hopefully has enough info)
using System.Collections.Generic;
using System;
using CoreApi;
using System.Collections;
using System.Windows.Input;
using Microsoft.Practices.Composite.Presentation.Commands;
using System.Windows;
public class MembershipCloseoutAgreementsViewModel : PropertyChangedBase, IMembershipCloseoutPartViewModel
{
public MembershipCloseoutAgreementsViewModel()
{
CloseAgreements = new DelegateCommand<object>((p) => CloseSelectedAgreements());
}
public void Initialize(PartInitializationInfo initializationInfo)
{
if (_backgroundWorker == null)
{
_backgroundWorker = new BackgroundWorker();
_backgroundWorker.DoWork += new DoWorkEventHandler(ProcessAgreements);
_backgroundWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(OnProcessCompleted);
}
}
private void CloseSelectedAgreements()
{
if (SelectedAgreements != null && SelectedAgreements.Count > 0)
{
Mouse.OverrideCursor = Cursors.Wait;
IsProcessing = true;
IsProcessEnabled = false;
IsGridEnabled = false;
_backgroundWorker.RunWorkerAsync();
}
}
private void ProcessAgreements(object sender, DoWorkEventArgs e)
{
if (numEAgreements > 0)
{
//this method will create and send requests
var success = ProcessEAgreementRequest(numEAgreements, SelectedAgreements);
...
//I've tried putting NotifyPropertyChanged(() => PersonAgreements) here and inside the "ProcessEAgreementRequest" method
}
/**Refresh the grid**/
if (PersOrgNbr.Item1 == "PERS")
{
PersonAgreements = AgreementDetailProvider.GetAgreementDetails(PersOrgNbr);
//I've tried putting NotifyPropertyChanged(() => PersonAgreements) here
}
else
{
PersonAgreements = AgreementDetailProvider.GetAgreementDetails(PersOrgNbr, MemberNumber.ToString(), AccountList);
//I've tried putting NotifyPropertyChanged(() => PersonAgreements) here
}
NotifyPropertyChanged(() => PersonAgreements);
}
private void OnProcessCompleted(object s, RunWorkerCompletedEventArgs e1)
{
IsProcessing = false;
IsProcessEnabled = false;
IsGridEnabled = true;
Mouse.OverrideCursor = null;
}
<ListView Grid.Row="2" x:Name="AgreementsList"
HorizontalAlignment="Right"
ItemsSource="{Binding PersonAgreements}"
IsEnabled="{Binding IsGridEnabled}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<ei:ChangePropertyAction TargetObject="{Binding Mode=OneWay}"
PropertyName="SelectedItems"
Value="{Binding Path=SelectedItems, ElementName=AgreementsList}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<ListView.View>
<GridView>
<GridViewColumn Header="Agreement #" Width="110" DisplayMemberBinding="{Binding AgreementNumber}"></GridViewColumn>
<GridViewColumn Header="Pers/Org#" Width="110" DisplayMemberBinding="{Binding AssociatedPersOrgNumber}"></GridViewColumn>
<GridViewColumn Header="Description" Width="575" DisplayMemberBinding="{Binding UI_Description}"></GridViewColumn>
</GridView>
</ListView.View>
</ListView>
'''
The issue was with the list not refreshing correctly. Instead of setting the list as a property I created a brand new list every time the code re-queried the database. When the list was a property it was persisting across all the calls and providing odd values. Either no change, or it appended more data. I believe this is lists are a 'reference' type as opposed to a 'value' type. Sneaky.

How to keep the selected item of a list view WPF binding

I have a function which updates every 5 seconds. I would like to keep the selected item but the item does not get selected.
The function below updates every 5 seconds and updates the list in the list view:
C#
public void festJSONUpdateEventHandler()
{
var tempfest = Workspace.This.festStats.Selectedfest;
//REFRESH BINDINGS HERE!!
Workspace.This.festStats.festItems = MainWindow._fest.festData.fest_Items;
Workspace.This.festStats.Selectedfest = tempfest;
}
XAML
<DataTemplate>
<StackPanel Orientation="Vertical">
<ListView x:Name="lvfest" ItemsSource="{Binding festItems}" SelectedItem="{Binding Selectedfest, Mode=TwoWay}" IsSynchronizedWithCurrentItem="True" >
<ListView.View>
<GridView>
<GridViewColumn Width="100" DisplayMemberBinding="{Binding id}" >
<GridViewColumn.Header>
<GridViewColumnHeader Tag="ID" Click="lvfestColumnHeader_Click">ID</GridViewColumnHeader>
</GridViewColumn.Header>
</GridViewColumn>
<GridViewColumn Width="100" DisplayMemberBinding="{Binding formatType}" >
<GridViewColumn.Header>
<GridViewColumnHeader Tag="Format" Click="lvfestColumnHeader_Click">Format</GridViewColumnHeader>
</GridViewColumn.Header>
</GridViewColumn>
<GridViewColumn Width="100" DisplayMemberBinding="{Binding modifiedIso8601}" >
<GridViewColumn.Header>
<GridViewColumnHeader Tag="Date" Click="lvfestColumnHeader_Click">Date</GridViewColumnHeader>
</GridViewColumn.Header>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</StackPanel>
</DataTemplate>
More C#
class festViewModel : ToolViewModel
{
public festItem _selectedfest;
public festItem Selectedfest
{
get { return _selectedfest; }
set
{
if (_selectedfest != value)
{
_selectedfest = value;
RaisePropertyChanged("Selectedfest");
}
}
}
private List<festItem> _festItems;
public List<festItem> festItems
{
get { return _festItems; }
set
{
if (_festItems != value)
{
_festItems = value;
RaisePropertyChanged("festItems");
}
}
}
}
From comments:
I have a library that extracts JSON data and in there, there is a list
of objects called fest_Items. from this list, I send the objects to
festItems list in the festViewModel which is bound to my list view.
Is the list being recreated at each function call (ergo 5 seconds)? It may be because your items do not contains the same reference to the object.
Two or more objects sharing the same values may be not the same references. If you are recreating the list, that may be the cause of the binding failing to retreive the item.
As a solution, I suggest finding an unique property (like the ID property of your model) and search the item in the newly created list.
Exemple:
public void festJSONUpdateEventHandler()
{
var tempfestID = Workspace.This.festStats.Selectedfest.ID;
//REFRESH BINDINGS HERE!!
Workspace.This.festStats.festItems = MainWindow._fest.festData.fest_Items;
// Find the first festStat that has the same ID as the old selected one.
Workspace.This.festStats.Selectedfest = Workspace.This.festStats.FirstOrDefault(x => x.ID == tempfestID);
}
Let me know if it is not working.

Bind datatable to listview wpf mvvm

I've tried almost all the soulutions provided on stackoverflow and can't seem to get it to work. Am new to wpf and mvvm and was trying to bind a datatable to a listview and below is my code.
//code for viewmodel
public DataTable RetrieveDetails
{
get
{
DataTable users = new DataTable();
string dataBaseName = "name.db3";
using (SQLiteConnection connection = new SQLiteConnection("Data Source=" + dataBaseName + "; Version=3;"))
{
var details = "SELECT * FROM users";
connection.Open();
SQLiteCommand cmd = new SQLiteCommand(details, connection);
cmd.ExecuteNonQuery();
SQLiteDataAdapter adapter = new SQLiteDataAdapter(details, connection);
adapter.SelectCommand.CommandTimeout = 120;
DataSet ds = new DataSet();
adapter.Fill(ds);
return ds.Tables[0];
}
}
}
And below is how i bind the datatable to my list view
<ListView x:Name="FormOneView" ItemsSource="{Binding Path=RetrieveDetails}">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding FirstName}" Header="First Name" />
</GridView>
</ListView.View>
</ListView>
I know this is supposed to be simple but am having a hard time displaying the data.
Try this approach:
<ListView x:Name="FormOneView" ItemsSource="{Binding}" DataContext="{Binding Path=allData}" HorizontalAlignment="Left" Height="100" Margin="243,289,0,0" VerticalAlignment="Top" Width="191">
<ListView.View>
<GridView>
<GridViewColumn Header="Time" Width="50" DisplayMemberBinding="{Binding Path=Tid}"/>
<GridViewColumn Header="Acceleration" Width="70" DisplayMemberBinding="{Binding Path=Acceleration}"/>
</GridView>
</ListView.View>
</ListView>
in this XAML I'm giving you an example of a datatable with two columns. I'm binding my ListView columns to my sql table column headers using DisplayMemberBinding.
Your backgournd code is actually fine, but you need to bind the DataContext of your ListView using propertyChangedEventHandler. Below is an example of how to do such a binding:
Add following two lines in your method:
adapter.Fill(ds, "users");
allData = ds.Tables["users"].DefaultView;
Then add following methods in your class. Your class should inherit INotifyPeropertyChanged
(yourClassName : INotifyPeropertyChanged) otherwise it won't work.
private DataView _allData;
public DataView allData
{
get { return _allData; }
set
{
if (value != _allData)
{
_allData = value;
OnPropertyChanged("allData");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
the result:
in order for the above code to work, need to add the following line of code
listview.datacontext = class holding alldata.allData

wpf xaml loop thru a gridview which is inside a listview and get the cell value

I have the following in my dgridtext.xaml file.
XAML
<Grid x:Name="grid1">
<StackPanel>
<ListView Name="listview1" IsTextSearchEnabled="True" TextSearch.TextPath="Enquiry_Number">
<ListView.View>
<GridView ColumnHeaderToolTip="Multiple Category Information">
<GridViewColumn DisplayMemberBinding="{Binding Path=Enquiry_Number}" Header="Enquiry number"/>
<GridViewColumn DisplayMemberBinding="{Binding Path=Consignee_Ref}" Header="Consignee reference"/>
<GridViewColumn DisplayMemberBinding="{Binding Path=Booking_Reference}" Header="Booking reference"/>
</GridView>
</ListView.View>
</ListView>
</StackPanel>
</Grid>
Something like this.
dgridtest.xaml.cs
for (int i = 0; i < listview1.Items.Count; i++)
{
MessageBox.Show(listview1.Items[i].ToString());
}
But all that returns is System.Data.DataRowView
Please help.
Update:
In My dgridtextxaml.cs file, I call the DataManager.cs class passing a dataset object which is the source of my listview(listview1).
DataManager.BindFilteredData(dts);
listview1.ItemsSource = dts.Tables[0].DefaultView;
And this is what I have in my DataManager.cs class
public static void BindFilteredData(DataSet dts)
{
using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["ConString"].ConnectionString))
{
string sql = "SELECT Enquiry_Number, Consignee_Ref, Booking_Reference FROM ConsHead";
using (SqlDataAdapter adapter = new SqlDataAdapter(sql, connection))
{
adapter.Fill(dts);
}
}
}
You can cast the item to whatever class you use in the ListView and then use any property you need from it. Something like:
var item = listView1.Items[i] as YourClassHere;
Edit
In your case, since you bind the ItemsSource to a DataSet directly you probably can use DataRowView class, and then use the properties from it:
var firstItem = listview1.Items[0] as DataRowView;
var firstCellValue = firstItem.Row[0];

Making databound ListViewColumns resize when adding an item

I'm making an WPF-application that has a databound ListView. When loading the application, the width of the ListViewColumns will auto-resize, but when adding or changing an item it doesn't auto-resize. I've tried refreshing the listview, setting the width of the column to auto, -1 or -2 in both xaml and VB-code and I've tries changing the itemssource to nothing before refilling it with items. This is the xaml code:
<ListView x:Name="lsvPersons" Margin="5,5,5,35" ItemsSource="{Binding Persons}">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding Name}" Header="Name"/>
<GridViewColumn DisplayMemberBinding="{Binding Gender}" Header="Gender"/>
<GridViewColumn DisplayMemberBinding="{Binding Age}" Header="Age"/>
</GridView>
</ListView.View>
</ListView>
<Button x:Name="btnAddPerson" Content="Add" Height="25" Margin="0,0,200,5" Width="80"/>
The binding works with a controller, which gets the persons with Person.getPersons from a SQL-database:
Private oController As New MainController()
Public Sub New()
MyBase.New()
Me.InitializeComponent()
Me.DataContext = oController
End Sub
After pressing the button, a window will open to add a person. After the window is closed, the item is added to the listview with following code, which refreshes the items in the listview:
lsvPersons.ItemsSource = Person.getPersons()
So, what do I need to do to auto-resize the columns of the listview when an item is added or editted?
GridView gv = lvSrchResulsGrid.View as GridView;
if (gv != null)
{
int colNum = 0;
foreach (GridViewColumn c in gv.Columns)
{
// Code below was found in GridViewColumnHeader.OnGripperDoubleClicked() event handler (using Reflector)
// i.e. it is the same code that is executed when the gripper is double clicked
// if (adjustAllColumns || App.StaticGabeLib.FieldDefsGrid[colNum].DispGrid)
// if (adjustAllColumns || fdGridSorted[colNum].AppliedDispGrid)
// {
if (double.IsNaN(c.Width))
{
c.Width = c.ActualWidth;
}
c.Width = double.NaN;
// }
}
}

Resources