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.
Related
I am looking for a way to know if my listview contains a value. Below is my code.
public class OnlineUserList
{
public string Name { get; set; }
public string Color { get; set; }
}
<ListView x:Name="lvOnlineUsers" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" SelectionMode="Single" SelectionChanged="lvOnlineUsers_SelectionChanged">
<ListView.View>
<GridView x:Name="lvOnlineUsersGridView" AllowsColumnReorder="False">
<GridViewColumn Header="Online Users" Block.TextAlignment="Center" TextOptions.TextFormattingMode="Display" TextBlock.FontWeight="Bold">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Name="tbOnlineUsersGridView" Text="{Binding Path=Name}" Foreground="{Binding Path=Color}" HorizontalAlignment="Center" VerticalAlignment="Center" TextOptions.TextFormattingMode="Display" Style="{StaticResource ResourceKey=lblLabel}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
public void AddUserToList(string username)
{
lvOnlineUsers.Items.Add(new OnlineUserList { Name = username, Color = "Black" });
}
Now this is where am having issue
public void RemoveUserFromList(string username)
{
if(lvOnlineUsers.Items.Contains(username))
lvOnlineUsers.Items.Remove(username);
}
You should learn MVVM.
In the mean time, put the items in an ObservableCollection and assign it to the listview's ItemsSource property in your codebehind. Thereafter, repeat after me: Never, ever touch lvOnlineUsers.Items. Never, never, never. Forget that it exists. Everything you do, you interact with the ObservableCollection. Search it, add items to it, remove items from it. The UI will magically and mysteriously update itself.
I'm going to assume this is in MainWindow. If this is in a different view, the constructor will have a different name.
public MainWindow()
{
InitializeComponent();
lvOnlineUsers.ItemsSource = _onlineUsers;
}
private ObservableCollection<OnlineUserList> _onlineUsers
= new ObservableCollection<OnlineUserList>();
public void AddUserToList(string username)
{
_onlineUsers.Add(new OnlineUserList { Name = username, Color = "Black" });
}
public void RemoveUserFromList(string username)
{
// We don't search _onlineUsers for the string username, because
// _onlineUsers doesn't contain strings. It contains your user class.
// So instead, we look for the user class instance that has the name
// we want.
var found = _onlineUsers.FirstOrDefault(ou => ou.Name == username);
if (found != null)
{
_onlineUsers.Remove(found);
}
}
Until you have looked into MVVM, try this:
for(int i = lvOnlineUsers.Items.Count - 1; i >= 0; --i)
{
OnlineUserList item = lvOnlineUsers.Items[i] as OnlineUserList;
if (item != null && item.Name == username)
lvOnlineUsers.Items.Remove(lvOnlineUsers.Items[i]);
}
I am having problem binding ObservableCollection as ItemsSource to a combo box (this combobox is in a listview of having some rows).
I followed A collection of StackPanel as ItemsSource of ComboBox but I did not get any clues for resolving my problem.
Problem:
I was able to add items to a combobox which is at the top on the form.
I have created a listview containing 3 text blocks and 1 combobox.
I am successful in populating data for the text blocks in listview.
But the problem lies with Combobox. First time, it shows all the items for each row in ListView. Once I select item or click on the combobox again to see the items, my list disappears. Only one combobox in the listview row shows all items. Other comboboxes shows blank.
Also, I was trying to save the index of the selected item and show the selected panel next time. But I did not get how to bind the stackpanel with selecteditem and selecteditemvalue.
I tried many ways of directly binding the items to the combobox in listview. But nothing worked. Request someone to help me on this.
Details of the code is given below:
I have XAML like below:
<Grid>
<Grid Height="40">
<ComboBox x:Name="cbList" />
</Grid>
<Grid Margin="0,56,0,168"></Grid>
<ListView x:Name="lvFirst" Margin="0,195,0,12">
<ListView.View>
<GridView >
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Width="50" x:Uid="tbListView1" Text="{Binding FirstName}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Width="50" x:Uid="tbListView2" Text="{Binding LastName}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Width="50" x:Uid="tbListView1" Text="{Binding ID}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<ComboBox Width="100" x:Uid="cbListView" ItemsSource="{Binding Path=SPForComboBox}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</Grid>
In the code behind I have a Contact class as below:
public class Contact : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
void Notify(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
private string _fn;
private string _ln;
public string FirstName
{
get
{ return _fn; }
set
{ _fn = value; Notify("FirstName"); }
}
public string LastName
{
get
{ return _ln; }
set
{ _ln = value; Notify("LastName"); }
}
private int _id;
public int ID
{
get { return _id; }
set { _id = value; Notify("ID"); }
}
public StackPanel sp;
public override string ToString()
{
return FirstName + " " + LastName;
}
private ObservableCollection<StackPanel> _sp;
public ObservableCollection<StackPanel> SPForComboBox
{
get { return _sp; }
set { _sp = value; Notify("SPForComboBox"); }
}
}
To populate cbList Items, I am repeatedly calling below function after Initialization() function:
private void AddColours(string name, byte hexcolor)
{
//Add this ShiftType to Combo box
SolidColorBrush rectangleBrush = new SolidColorBrush();
Color color = new Color();
color.A = hexcolor;
rectangleBrush.Color = color;
System.Windows.Controls.StackPanel stkPanel = new StackPanel(); //stack panel to hold rectangle + text
stkPanel.Orientation = Orientation.Horizontal;
cbList.Items.Add(stkPanel);
Rectangle colorRect = new Rectangle(); //rectangle showing colour for shift
colorRect.Height = 12;
colorRect.Width = colorRect.Height;
colorRect.Fill = rectangleBrush;
stkPanel.Children.Add(colorRect);
System.Windows.Controls.TextBlock cboText = new TextBlock(); //Name of shift
cboText.Text = name;
cboText.Margin = new Thickness(5, 5, 5, 5);
stkPanel.Children.Add(cboText);
}
In the main window class, I have a created an ObservableCollection object as public static (object name is contacts).
public static ObservableCollection<Contact> contacts = new ObservableCollection<Contact>();
After the Initialization() function, I am populating contacts as below:
AddColours("First", 100);
AddColours("Second", 50);
AddColours("Third", 20);
AddColours("Fourth", 0);
AddColours("Fifth", 80);
Contact c1 = new Contact();
c1.FirstName = "Digo";
c1.LastName = "Maradona";
c1.ID = 0;
c1.SPForComboBox = new ObservableCollection<StackPanel>();
foreach (StackPanel sp in cbList.Items)
{
c1.SPForComboBox.Add(sp);
}
Contact c2 = new Contact();
c2.FirstName = "Brian";
c2.LastName = "Lara";
c2.ID = 1;
c2.SPForComboBox = new ObservableCollection<StackPanel>();
foreach (StackPanel sp in cbList.Items)
{
c2.SPForComboBox.Add(sp);
}
Contact c3 = new Contact();
c3.FirstName = "Sachin";
c3.LastName = "Tendulkar";
c3.ID = 2;
c3.SPForComboBox = new ObservableCollection<StackPanel>();
foreach (StackPanel sp in cbList.Items)
{
c3.SPForComboBox.Add(sp);
}
contacts.Add(c1);
contacts.Add(c2);
contacts.Add(c3);
lvFirst.ItemsSource = contacts;
HighCore, Thank you very much for the links. I have existing implementation and adding combobox to that.
I too felt that the method followed is not good. I shall certainly look at the alternatives provided by you and suggested by pushpraj.
Answer:
I thought referring objects in other combobox will work till the items exist in that combobox. But I need to create rectangle and textblock for reach combobox I am creating and for each entry in that combobox.
So certainly I need to do it in foreach loop.
Also, once I do this I can use SelectedIndex referring to the integer and SelectedItem to sp (individual stackpanel in that class).
This method is not good to follow but might be helpful for somebody.
Thanks.
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.
I'm creating a WPF application using the MVVM design pattern that consists of a ListView and some ComboBoxes. The ComboBoxes are used to filter the ListView. What I am trying to accomplish is populating the combobox with items in the related ListView column. In other words, if my ListView has Column1, Column2, and Column3, I want ComboBox1 to display all UNIQUE items in Column1. Once an item is selected in the ComboBox1, I want the items in ComboBox2 and ComboBox3 to be filtered based on ComboBox1's selection, meaning that ComboBox2 and ComboBox3 can only contain valid selections. This would be somewhat similar to a CascadingDropDown control if using the AJAX toolkit in ASP.NET, except the user can select any ComboBox at random, not in order.
My first thought was to bind ComboBoxes to the same ListCollectionView that the ListView is bound to, and set the DisplayMemberPath to the appropriate column. This works great as far as filtering the ListView and ComboBoxes together goes, but it displays all items in the ComboBox rather than just the unique ones (obviously). So my next thought was to use a ValueConverter to only return the only the unique items, but I have not been sucessful.
FYI: I read Colin Eberhardt's post on adding a AutoFilter to a ListView on CodeProject, but his method loops through each item in the entire ListView and adds the unique ones to a collection. Although this method works, it seems that it would be very slow for large lists.
Any suggestions on how to achieve this elegantly? Thanks!
Code Example:
<ListView ItemsSource="{Binding Products}" SelectedItem="{Binding SelectedProduct}">
<ListView.View>
<GridView>
<GridViewColumn Header="Item" Width="100" DisplayMemberBinding="{Binding ProductName}"/>
<GridViewColumn Header="Type" Width="100" DisplayMemberBinding="{Binding ProductType}"/>
<GridViewColumn Header="Category" Width="100" DisplayMemberBinding="{Binding Category}"/>
</GridView>
</ListView.View>
</ListView>
<StackPanel Grid.Row="1">
<ComboBox ItemsSource="{Binding Products}" DisplayMemberPath="ProductName"/>
<ComboBox ItemsSource="{Binding Products}" DisplayMemberPath="ProductType"/>
<ComboBox ItemsSource="{Binding Products}" DisplayMemberPath="Category"/>
</StackPanel>
Check this out:
<Window x:Class="DistinctListCollectionView.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DistinctListCollectionView"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<local:PersonCollection x:Key="data">
<local:Person FirstName="aaa" LastName="xxx" Age="1"/>
<local:Person FirstName="aaa" LastName="yyy" Age="2"/>
<local:Person FirstName="aaa" LastName="zzz" Age="1"/>
<local:Person FirstName="bbb" LastName="xxx" Age="2"/>
<local:Person FirstName="bbb" LastName="yyy" Age="1"/>
<local:Person FirstName="bbb" LastName="kkk" Age="2"/>
<local:Person FirstName="ccc" LastName="xxx" Age="1"/>
<local:Person FirstName="ccc" LastName="yyy" Age="2"/>
<local:Person FirstName="ccc" LastName="lll" Age="1"/>
</local:PersonCollection>
<local:PersonAutoFilterCollection x:Key="data2" SourceCollection="{StaticResource data}"/>
<DataTemplate DataType="{x:Type local:Person}">
<WrapPanel>
<TextBlock Text="{Binding FirstName}" Margin="5"/>
<TextBlock Text="{Binding LastName}" Margin="5"/>
<TextBlock Text="{Binding Age}" Margin="5"/>
</WrapPanel>
</DataTemplate>
</Window.Resources>
<DockPanel>
<WrapPanel DockPanel.Dock="Top">
<ComboBox DataContext="{Binding Source={StaticResource data2}, Path=Filters[0]}" ItemsSource="{Binding DistinctValues}" SelectedItem="{Binding Value}" Width="120"/>
<ComboBox DataContext="{Binding Source={StaticResource data2}, Path=Filters[1]}" ItemsSource="{Binding DistinctValues}" SelectedItem="{Binding Value}" Width="120"/>
<ComboBox DataContext="{Binding Source={StaticResource data2}, Path=Filters[2]}" ItemsSource="{Binding DistinctValues}" SelectedItem="{Binding Value}" Width="120"/>
</WrapPanel>
<ListBox ItemsSource="{Binding Source={StaticResource data2}, Path=FilteredCollection}"/>
</DockPanel>
</Window>
And the view model:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.ComponentModel;
namespace DistinctListCollectionView
{
class AutoFilterCollection<T> : INotifyPropertyChanged
{
List<AutoFilterColumn<T>> filters = new List<AutoFilterColumn<T>>();
public List<AutoFilterColumn<T>> Filters { get { return filters; } }
IEnumerable<T> sourceCollection;
public IEnumerable<T> SourceCollection
{
get { return sourceCollection; }
set
{
if (sourceCollection != value)
{
sourceCollection = value;
CalculateFilters();
}
}
}
void CalculateFilters()
{
var propDescriptors = typeof(T).GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
foreach (var p in propDescriptors)
{
Filters.Add(new AutoFilterColumn<T>()
{
Parent = this,
Name = p.Name,
Value = null
});
}
}
public IEnumerable GetValuesForFilter(string name)
{
IEnumerable<T> result = SourceCollection;
foreach (var flt in Filters)
{
if (flt.Name == name) continue;
if (flt.Value == null || flt.Value.Equals("All")) continue;
var pdd = typeof(T).GetProperty(flt.Name);
{
var pd = pdd;
var fltt = flt;
result = result.Where(x => pd.GetValue(x, null).Equals(fltt.Value));
}
}
var pdx = typeof(T).GetProperty(name);
return result.Select(x => pdx.GetValue(x, null)).Concat(new List<object>() { "All" }).Distinct();
}
public AutoFilterColumn<T> GetFilter(string name)
{
return Filters.SingleOrDefault(x => x.Name == name);
}
public IEnumerable<T> FilteredCollection
{
get
{
IEnumerable<T> result = SourceCollection;
foreach (var flt in Filters)
{
if (flt.Value == null || flt.Value.Equals("All")) continue;
var pd = typeof(T).GetProperty(flt.Name);
{
var pdd = pd;
var fltt = flt;
result = result.Where(x => pdd.GetValue(x, null).Equals(fltt.Value));
}
}
return result;
}
}
internal void NotifyAll()
{
foreach (var flt in Filters)
flt.Notify();
OnPropertyChanged("FilteredCollection");
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string prop)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
#endregion
}
class AutoFilterColumn<T> : INotifyPropertyChanged
{
public AutoFilterCollection<T> Parent { get; set; }
public string Name { get; set; }
object theValue = null;
public object Value
{
get { return theValue; }
set
{
if (theValue != value)
{
theValue = value;
Parent.NotifyAll();
}
}
}
public IEnumerable DistinctValues
{
get
{
var rc = Parent.GetValuesForFilter(Name);
return rc;
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string prop)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
#endregion
internal void Notify()
{
OnPropertyChanged("DistinctValues");
}
}
}
The other classes:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace DistinctListCollectionView
{
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace DistinctListCollectionView
{
class PersonCollection : List<Person>
{
}
class PersonAutoFilterCollection : AutoFilterCollection<Person>
{
}
}
If you are using MVVM, then all of your bound data objects are in your ViewModel class, and your ViewModel class is implementing INotifyPropertyChanged, correct?
If so, then you can maintain state variables for SelectedItemType1, SelectedItemType2, etc., that are bound to your ComboBox(es) SelectedItem dependency property. In the Setter for SelectedItemType1, populate the List property (which is bound to the ItemsSource for ComboBoxType2) and fire the NotifyPropertyChanged for the List property. Repeat this for Type3 and you should be in the ballpark.
As for the "refresh" issue, or how does the View know when something has changed, it all comes down to the binding mode and firing the NotifyPropertyChanged event at the correct moments.
You could do this with a ValueConverter, and I love ValueConverters, but I think in this case it is more elegant to manage your ViewModel so that the Binding just happens.
Why not just create another property that contained only the distinct values from the list using a linq query or something like that?
public IEnumerable<string> ProductNameFilters
{
get { return Products.Select(product => product.ProductName).Distinct(); }
}
...etc.
You'll have to raise property changed notifications for each of the filter lists when your Product property changes, but that's not a big deal.
You should really consider your ViewModel as a big ValueConverter for your view. The only time I would use a ValueConverter in MVVM is when I need to change data from a data type that is not view-specific to one that is view-specific. Example: for values greater than 10, text needs to be Red and for values Less than 10, text needs to be Blue... Blue and Red are view-specific types and shouldn't be something that gets returned from a ViewModel. This is really the only case where this logic shouldn't be in a ViewModel.
I question the validity of the "very slow for large lists" comment... generally the "large" for humans and "large" for a computer are two very different things. If you are in the realm of "large" for both computers and humans, I would also question showing this much data on a screen. Point being, it's likely not large enough for you to notice the cost of these queries.
I am creating a WPF window with a DataGrid, and I want to show the blank "new item" row at the bottom of the grid that allows me to add a new item to the grid. For some reason, the blank row is not shown on the grid on my window. Here is the markup I used to create the DataGrid:
<toolkit:DataGrid x:Name="ProjectTasksDataGrid"
DockPanel.Dock="Top"
Style="{DynamicResource {x:Static res:SharedResources.FsBlueGridKey}}"
AutoGenerateColumns="False"
ItemsSource="{Binding SelectedProject.Tasks}"
RowHeaderWidth="0"
MouseMove="OnStartDrag"
DragEnter="OnCheckDropTarget"
DragOver="OnCheckDropTarget"
DragLeave="OnCheckDropTarget"
Drop="OnDrop"
InitializingNewItem="ProjectTasksDataGrid_InitializingNewItem">
<toolkit:DataGrid.Columns>
<toolkit:DataGridCheckBoxColumn HeaderTemplate="{DynamicResource {x:Static res:SharedResources.CheckmarkHeaderKey}}" Width="25" Binding="{Binding Completed}" IsReadOnly="false"/>
<toolkit:DataGridTextColumn Header="Days" Width="75" Binding="{Binding NumDays}" IsReadOnly="false"/>
<toolkit:DataGridTextColumn Header="Due Date" Width="75" Binding="{Binding DueDate, Converter={StaticResource standardDateConverter}}" IsReadOnly="false"/>
<toolkit:DataGridTextColumn Header="Description" Width="*" Binding="{Binding Description}" IsReadOnly="false"/>
</toolkit:DataGrid.Columns>
</toolkit:DataGrid>
I can't figure out why the blank row isn't showing. I have tried the obvious stuff (IsReadOnly="false", CanUserAddRows="True"), with no luck. Any idea why the blank row is disabled? Thanks for your help.
You must also have to have a default constructor on the type in the collection.
Finally got back to this one. I am not going to change the accepted answer (green checkmark), but here is the cause of the problem:
My View Model wraps domain classes to provide infrastructure needed by WPF. I wrote a CodeProject article on the wrap method I use, which includes a collection class that has two type parameters:
VmCollection<VM, DM>
where DM is a wrapped domain class, and DM is the WPF class that wraps it.
It truns out that, for some weird reason, having the second type parameter in the collection class causes the WPF DataGrid to become uneditable. The fix is to eliminate the second type parameter.
Can't say why this works, only that it does. Hope it helps somebody else down the road.
Vincent Sibal posted an article describing what is required for adding new rows to a DataGrid. There are quite a few possibilities, and most of this depends on the type of collection you're using for SelectedProject.Tasks.
I would recommend making sure that "Tasks" is not a read only collection, and that it supports one of the required interfaces (mentioned in the previous link) to allow new items to be added correctly with DataGrid.
In my opinion this is a bug in the DataGrid. Mike Blandford's link helped me to finally realize what the problem is: The DataGrid does not recognize the type of the rows until it has a real object bound. The edit row does not appear b/c the data grid doesn't know the column types. You would think that binding a strongly typed collection would work, but it does not.
To expand upon Mike Blandford's answer, you must first assign the empty collection and then add and remove a row. For example,
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// data binding
dataGridUsers.ItemsSource = GetMembershipUsers();
EntRefUserDataSet.EntRefUserDataTable dt = (EntRefUserDataSet.EntRefUserDataTable)dataGridUsers.ItemsSource;
// hack to force edit row to appear for empty collections
if (dt.Rows.Count == 0)
{
dt.AddEntRefUserRow("", "", false, false);
dt.Rows[0].Delete();
}
}
Add an empty item to your ItemsSource and then remove it. You may have to set CanUserAddRows back to true after doing this. I read this solution here: (Posts by Jarrey and Rick Roen)
I had this problem when I set the ItemsSource to a DataTable's DefaultView and the view was empty. The columns were defined though so it should have been able to get them. Heh.
This happned to me , i forgot to new up the instance and it was nightmare for me . once i created an instance of the collection in onviewloaded it was solved.
`observablecollection<T> _newvariable = new observablecollection<T>();`
this solved my problem. hope it may help others
For me the best way to implement editable asynchronous DataGrid looks like that:
View Model:
public class UserTextMainViewModel : ViewModelBase
{
private bool _isBusy;
public bool IsBusy
{
get { return _isBusy; }
set
{
this._isBusy = value;
OnPropertyChanged();
}
}
private bool _isSearchActive;
private bool _isLoading;
private string _searchInput;
public string SearchInput
{
get { return _searchInput; }
set
{
_searchInput = value;
OnPropertyChanged();
_isSearchActive = !string.IsNullOrEmpty(value);
ApplySearch();
}
}
private ListCollectionView _translationsView;
public ListCollectionView TranslationsView
{
get
{
if (_translationsView == null)
{
OnRefreshRequired();
}
return _translationsView;
}
set
{
_translationsView = value;
OnPropertyChanged();
}
}
private void ApplySearch()
{
var view = TranslationsView;
if (view == null) return;
if (!_isSearchActive)
{
view.Filter = null;
}
else if (view.Filter == null)
{
view.Filter = FilterUserText;
}
else
{
view.Refresh();
}
}
private bool FilterUserText(object o)
{
if (!_isSearchActive) return true;
var item = (UserTextViewModel)o;
return item.Key.Contains(_searchInput, StringComparison.InvariantCultureIgnoreCase) ||
item.Value.Contains(_searchInput, StringComparison.InvariantCultureIgnoreCase);
}
private ICommand _clearSearchCommand;
public ICommand ClearSearchCommand
{
get
{
return _clearSearchCommand ??
(_clearSearchCommand =
new DelegateCommand((param) =>
{
this.SearchInput = string.Empty;
}, (p) => !string.IsNullOrEmpty(this.SearchInput)));
}
}
private async void OnRefreshRequired()
{
if (_isLoading) return;
_isLoading = true;
IsBusy = true;
try
{
var result = await LoadDefinitions();
TranslationsView = new ListCollectionView(result);
}
catch (Exception ex)
{
//ex.HandleError();//TODO: Needs to create properly error handling
}
_isLoading = false;
IsBusy = false;
}
private async Task<IList> LoadDefinitions()
{
var translatioViewModels = await Task.Run(() => TranslationRepository.Instance.AllTranslationsCache
.Select(model => new UserTextViewModel(model)).ToList());
return translatioViewModels;
}
}
XAML:
<UserControl x:Class="UCM.WFDesigner.Views.UserTextMainView"
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"
xmlns:model="clr-namespace:Cellebrite.Diagnostics.Model.Entities;assembly=Cellebrite.Diagnostics.Model"
xmlns:System="clr-namespace:System;assembly=mscorlib"
xmlns:converters1="clr-namespace:UCM.Infra.Converters;assembly=UCM.Infra"
xmlns:core="clr-namespace:UCM.WFDesigner.Core"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="300">
<DockPanel>
<StackPanel Orientation="Horizontal"
DockPanel.Dock="Top"
HorizontalAlignment="Left">
<DockPanel>
<TextBlock Text="Search:"
DockPanel.Dock="Left"
VerticalAlignment="Center"
FontWeight="Bold"
Margin="0,0,5,0" />
<Button Style="{StaticResource StyleButtonDeleteCommon}"
Height="20"
Width="20"
DockPanel.Dock="Right"
ToolTip="Clear Filter"
Command="{Binding ClearSearchCommand}" />
<TextBox Text="{Binding SearchInput, UpdateSourceTrigger=PropertyChanged}"
Width="500"
VerticalContentAlignment="Center"
Margin="0,0,2,0"
FontSize="13" />
</DockPanel>
</StackPanel>
<Grid>
<DataGrid ItemsSource="{Binding Path=TranslationsView}"
AutoGenerateColumns="False"
SelectionMode="Single"
CanUserAddRows="True">
<DataGrid.Columns>
<!-- your columns definition is here-->
</DataGrid.Columns>
</DataGrid>
<!-- your "busy indicator", that shows to user a message instead of stuck data grid-->
<Border Visibility="{Binding IsBusy,Converter={converters1:BooleanToSomethingConverter TrueValue='Visible', FalseValue='Collapsed'}}"
Background="#50000000">
<TextBlock Foreground="White"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Text="Loading. . ."
FontSize="16" />
</Border>
</Grid>
</DockPanel>
This pattern allows to work with data grid in a quite simple way and code is very simple either.
Do not forget to create default constructor for class that represents your data source.