- Home
- LLBLGen Pro
- Bugs & Issues
Error sorting WPF Datagrid on sub-properties of items in EntityCollection<item>
Joined: 27-Oct-2014
Version and Build: LLBLGen Pro v4.2 Build 08272014 fileversion 4.2.14.811
When we try to sort a WPF (ComponentOne) datagrid on a sub-property of an entitycollection item, the sorting fails. Grouping is also not available because sorting is failing.. The error is outside of the datagrid.
IF we change the itemsource for the grid from an EntityCollection to a List by using the linq ToList() method, then the grid will sort and group just fine on exactly the same entities.... so the issue is with the entitycollection / entityview2.
In the example below, it is the 'DriverName', 'Location' and 'Active' columns that cannot be sorted or grouped... all fail with similar errors. We have other examples for other views that fail the same way. No where can we seem to have sorting working on properties of properties of an entitycollection.
We have tried setting SortMemberPath for the grid columns to various things.... just he subproperty name, the full path, etc. But with this not being a problem if we ToList the source, the problem really seems to be inside the entitycollection / entityview2
We prefer not to add the performance hit of converting the entitycollections to Lists / etc to make this work. Also, adding properties to partial classes for the entities to 'copy in' the subproperties is not a good workaround in our opinion...
but it would work.
So please advise what you can do to help with this bug.
Exception:
System.ArgumentException occurred
_HResult=-2147024809
_message='UserEntityCollection' type does not have property named 'UserDriver.Name', so cannot sort data collection.
HResult=-2147024809
IsTransient=false
Message='UserEntityCollection' type does not have property named 'UserDriver.Name', so cannot sort data collection.
Source=PresentationFramework
StackTrace:
at System.Windows.Data.BindingListCollectionView.ConvertSortDescriptionCollection(SortDescriptionCollection sorts)
InnerException:
Using Adapter, .NET 4.0
MS SQL 2008 R2
public class ExtendedC1DataGrid : C1DataGrid
{
static ExtendedC1DataGrid()
{
}
public ExtendedC1DataGrid()
:base()
{
this.SelectionChanged += ExtendedC1DataGrid_SelectionChanged;
DefaultStyleKey = typeof(ExtendedC1DataGrid);
}
~ExtendedC1DataGrid()
{
this.SelectionChanged -= ExtendedC1DataGrid_SelectionChanged;
}
void ExtendedC1DataGrid_SelectionChanged(object sender, DataGridSelectionChangedEventArgs e)
{
C1DataGrid grid = sender as C1DataGrid;
grid.BringSelectedItemIntoView();
}
}
<C1Extended:ExtendedC1DataGrid
x:Name="c1dgUserDrivers"
Grid.Row="2"
AutoGenerateColumns="False"
CanUserRemoveRows="False"
CanUserAddRows="False"
HeadersVisibility="Column"
ItemsSource="{Binding Path=Users}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Style="{StaticResource {x:Static resC1:ObjectResources.DataGridLargeStyle}}">
<C1Extended:ExtendedC1DataGrid.Resources>
<ui:BindingProxy x:Key="proxy" Data="{Binding}"/>
</C1Extended:ExtendedC1DataGrid.Resources>
<C1Extended:ExtendedC1DataGrid.Columns>
<c1:DataGridTextColumn
Binding="{Binding Path=Username}"
Header="{x:Static transCore:Translations.UsernameLabel}"
IsReadOnly="True"
MinWidth="80"/>
<c1:DataGridTextColumn
Binding="{Binding Path=Name}"
Header="{x:Static transCore:Translations.Name}"
IsReadOnly="True"
MinWidth="80"/>
<c1:DataGridTextColumn
Binding="{Binding Path=UserDriver.Name}"
Header="{x:Static transCore:Translations.DriverName}"
MinWidth="80"/>
<c1:DataGridComboBoxColumn
Header="{x:Static transCore:Translations.Location}"
SelectedValuePath="Id"
DisplayMemberPath="Name"
Binding="{Binding Path=UserDriver.LocationId, Mode=TwoWay}"
ItemsSource="{Binding Path=Data.Locations, Source={StaticResource proxy}}"
MinWidth="120"/>
<c1:DataGridCheckBoxColumn
Binding="{Binding Path=UserDriver.Inactive, Converter={StaticResource _inverseBoolConverter}}"
Header="{x:Static transCore:Translations.Active}"
MinWidth="35"/>
</C1Extended:ExtendedC1DataGrid.Columns>
</C1Extended:ExtendedC1DataGrid>
public partial class UserDriverView : UserControl
{
public UserDriverView()
{
InitializeComponent();
}
}
internal class UserDriverViewModel : ViewModelBase
{
#region Fields
private List<LocationEntity> _locations;
private EntityCollection<UserEntity> _users;
#endregion
#region Constructor
public UserDriverViewModel(IEventAggregator eventAggregator)
: base(eventAggregator)
{
FetchData();
}
protected override void OnDispose()
{
if (_locations != null)
{
_locations.Clear();
}
_locations = null;
if (_users != null)
{
_users.Clear();
}
_users = null;
}
#endregion
#region Properties
public List<LocationEntity> Locations
{
get
{
return _locations;
}
}
public EntityCollection<UserEntity> Users
{
get { return _users; }
set
{
_users = value;
this.OnPropertyChanged(() => Users);
}
}
#endregion
#region Private Methods
private void FetchData()
{
IsLoadingData = true;
new Task(() =>
{
_locations = UserDriverBL.GetLocationsWithAll();
_users = UserDriverBL.GetUsersWithDrivers();
this.OnPropertyChanged(() => this.Locations);
this.OnPropertyChanged(() => this.Users);
IsLoadingData = false;
}).Start();
}
#endregion
}
SQL for the tables that produce the UserDriver, User and Location entities through the LLBLGen 4.2 GUI
CREATE TABLE [dbo].[User]
(
[Id] UNIQUEIDENTIFIER NOT NULL PRIMARY KEY DEFAULT NEWSEQUENTIALID() rowguidcol,
[Username] [nvarchar](256) NOT NULL,
[Password] [nvarchar](128) NOT NULL,
[IsActive] [bit] NOT NULL ,
[LastLoginDate] [datetime] NULL,
[IsWindowUser] [bit] NOT NULL ,
[Name] NVARCHAR(50) NOT NULL,
[LastUpdated] [datetime] NOT NULL ,
CONSTRAINT [IX_User_UserName] UNIQUE NONCLUSTERED
(
[Username] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
)
GO
CREATE INDEX [IX_User_User] ON [dbo].[User] (Name ASC)
GO
CREATE TABLE [dbo].[UserDriver] (
[UserId] UNIQUEIDENTIFIER NOT NULL,
[LocationId] INT NULL,
[Name] NVARCHAR(50) NOT NULL,
[Inactive] BIT NOT NULL,
[DgId] INT NOT NULL IDENTITY,
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([UserId] ASC),
CONSTRAINT [UQ_User_UserName] UNIQUE NONCLUSTERED ([Name] ASC, [LocationId] ASC),
CONSTRAINT [FK_UserDriver_User] FOREIGN KEY ([UserId]) REFERENCES [User]([Id]),
CONSTRAINT [FK_UserDriver_Location] FOREIGN KEY ([LocationId]) REFERENCES [Location]([Id])
);
GO
CREATE TABLE [dbo].[Location] (
[Id] INT NOT NULL,
[Name] NVARCHAR(40) NOT NULL,
[PremisesId] NVARCHAR(9) NULL,
CONSTRAINT [PK_Location] PRIMARY KEY CLUSTERED ([Id] ASC)
);
GO
BL and db methods to 'GetUsersWithDrivers'
public static EntityCollection<UserEntity> GetUsersWithDrivers()
{
using (var db = new DbUserDriverService())
{
var users = db.GetUsersWithDrivers();
foreach (var user in users)
{
if (user.UserDriver == null)
{
user.UserDriver = EntityFactory.CreateUserDriverEntity();
user.UserDriver.Inactive = true;
user.UserDriver.IsVirtual = true;
}
}
return users;
}
}
public EntityCollection<UserEntity> GetUsersWithDrivers()
{
var userList = new EntityCollection<UserEntity>();
using (var adapter = DatabaseAdapter)
{
var db = new LinqMetaData(adapter);
var query = from u in db.User
join ud in db.UserDriver on u.Id equals ud.UserId into ud_join
from ud in ud_join.DefaultIfEmpty()
select new
{
User = u,
Driver = ud,
IsDriverNull = (ud == null),
};
var qlist = query.ToList();
foreach (var item in qlist)
{
if (!item.IsDriverNull)
{
item.User.UserDriver = item.Driver;
}
userList.Add(item.User);
}
return userList;
}
}
public static UserDriverEntity CreateUserDriverEntity()
{
return CreateUserDriverEntity(new UserDriverEntity());
}
public static UserDriverEntity CreateUserDriverEntity(UserDriverEntity entity)
{
entity.Name = entity.Name;
entity.Inactive = (entity.Fields.GetCurrentValue((int)UserDriverFieldIndex.Inactive) == null) ? false : entity.Inactive;
return entity;
}
This is a 1:1 relation, correct? Or is it a 1:m relation? Please check the Designer for the type of the relation specified.
Also check the generated code for the Browsable attribute value on the UserDriver navigator inside the User entity. Is it true or false?
Joined: 27-Oct-2014
Hi,
Yes the relation is a 1:1 and the Browsable attribute is set to True.
Cheers
Does that change if you write a custom property on UserEntity that maps to the related property?
public string DriverName
{
get { return this.Driver.Name; }
}
Does it work if you use another grid? (i.e. the default wpf grid).
In which part exactly did you convert did you convert the results to a list? I see you always return a collection. Please clarify that.
What is the exception thrown? (message and stacktrace).
Also what's 'UserEntityCollection' ? It's in the exception, but not in the code anywhere. I assumed seeing that that you're using selfservicing, but that's not the case, your code shows you're using adapter, so what exactly is 'UserEntityCollection' ?
Joined: 27-Oct-2014
daelmo: Yes mapping to a first level property like you suggest works (and I did mention that in our original post) - but this not ideal to do going forward for an enterprise level application.
Using WPF datagrid does not help, same problem, same error. So no help there. Example below based on another of our grids.
<DataGrid
Grid.Row="2"
ItemsSource="{Binding Path=RolePermissions}">
<DataGrid.Columns>
<DataGridTextColumn
Header="Role.Name"
Binding="{Binding Path=Role.Name}"
Width="100"/>
<DataGridTextColumn
Header="RoleName"
Binding="{Binding Path=RoleName}"
Width="100"/>
</DataGrid.Columns>
</DataGrid>
RoleName is the mapped property we added, Role.Name is the original sub-property. We can sort on RoleName, but not Role.Name (same error)
As for how we tested the collection as a list, I made this change to the code:
public List<UserEntity> Users
{
get { return _users.ToList(); }
set
{
_users = value;
this.OnPropertyChanged(() => Users);
}
}
On the exception, I'm not sure what you are asking as I posted it in the orginial post. There was no exception when the collection was converted to a list int he getter.
System.ArgumentException occurred _HResult=-2147024809 _message='UserEntityCollection' type does not have property named 'UserDriver.Name', so cannot sort data collection. HResult=-2147024809 IsTransient=false Message='UserEntityCollection' type does not have property named 'UserDriver.Name', so cannot sort data collection. Source=PresentationFramework StackTrace: at System.Windows.Data.BindingListCollectionView.ConvertSortDescriptionCollection(SortDescriptionCollection sorts) InnerException:
Otis: Also what's 'UserEntityCollection' - I was hoping you could tell us! I spent hours looking for it after seeing this error. It is not in our code, and I could not find it under watches... so I assumed it was internal to the grid??? Like an on the fly collection created for sorting or something??? And yes we are using Adapter... and not self-servicing.
Joined: 27-Oct-2014
for completeness, here is the error we get in that wpf datagrid test when we sort on the Role.Name column:
'RolePermissionEntityCollection' type does not have property named 'Role.Name', so cannot sort data collection.
I think the error comes from the fact that the grid wants to perform IBindingList based sorting (so it doesn't do sorting by itself, it sorts by telling the bound data container to sort on a field), and that doesn't work, as the related field is in an object related to the bound object, not the object itself. (bound set of orders, sorting on .Customer.CompanyName has a problem, if the sorting is to be done by the bound object, which is an EntityView if you bind an entity collection).
Therefore, I asked whether binding a List<UserEntity> instead of an entity collection has the same problem. Did you test that?
Our code doesn't create collections on the fly for sorting, so I assume the grid created that collection with the name 'UserEntityCollection'. I'll see if I can repro it with our wpf example.
I can reproduce it with our WPF / Datascope example. When I bind the Customer.Orders directly through a List<OrderEntity> to the grid, to which I have hadded Customer.CompanyName as extra column, I can sort the column. When I bind the Customer.Orders through the binding system in XAML, it fails, as it then binds through IListSource on the entity collection and the grid binds to the EntityView2<OrderEntity> returned by the entity collection.
The EntityView(2) classes implement ITypedList and produce a list of property descriptors for bound controls. However the views also implement IBindingList and that's the problem here: the sorting fails, as there's no property descriptor for the related field, so it can't pass something to the IBindingList sort method.
When I bind the collection through an BindingList<OrderEntity> it fails to sort too, as sorting isn't implemented by the BindingList and it returns false on the property 'SupportsSorting'. We return true as we do support sorting. However the code never reaches the apply sorting method... So somewhere there's a disconnect within the controls with this: it is supposed to call the bound collection for sorting, but it can't get to that as it can't find the field to sort on (which is logical, as it isn't in the bound type) and therefore can't produce the property descriptor and a crash follows. Interesting that when a collection that doesn't implement IBindingList is bound, it can sort, namely when a List<T> is bound, it sorts the values in the grid. If we return 'false' there's no sorting at all in the grid, so it decides when to sort with the values it has when there's IBindingList available on the bound collection. I.o.w.: we can't do anything to solve this
I'm not sure whether componentone's grid has a feature where it can sort on the values in the grid instead of the IBindingList.sorting stuff. Otherwise it's either adding the fields to sort on as 'field mapped onto related fields' in the designer, or bind through List<T>.
Joined: 27-Oct-2014
Otis, thanks very much for your effort looking into that. Knowing the problem and the options, I did one last search and found a solution that we'll be using. This prevents us having to convert to a list in the view model, or creating extra properties.
All done in XAML In the resources:
<CollectionViewSource x:Key="rolePermissionsViewSource" Source="{Binding Path=RolePermissions}" CollectionViewType="ListCollectionView" />
Then in the grid:
DataContext="{StaticResource rolePermissionsViewSource}"
ItemsSource="{Binding}"
I guess by default the items source would treated as a BindingListCollectionView since it supports IBindingList, now we are just saying to treat it as a ListCollectionView instead, so now the grid is doing the sorting which we knew would work. The only thing left to do is to set the SortMemberPath on any columns which are a template column.
Thanks again!