I'd like to display data from database using an OData endpoint in the GridView. For this I've decided to use the QueryableDataServiceCollectionView, passing the DataServiceContext and DataServiceQuery into constructor.
For OData, I have a standard setup of Web Api OData Service endpoint and WCF Data Services 5 WPF Client. Model (DTO) looks like this:
[DataServiceKey(
"FactTradeHeaderIdentifier"
)]
public
class
TradeHeaderModel
{
[Key]
[DataMember(IsRequired =
true
)]
public
int
FactTradeHeaderIdentifier {
get
;
set
; }
...
}
And the autogenerated client side counterpart like this:
/// <summary>
/// There are no comments for Edft.Regulatory.Tracker.Service.Web.ApiModels.TradeHeaderModel in the schema.
/// </summary>
/// <KeyProperties>
/// FactTradeHeaderIdentifier
/// </KeyProperties>
[global::System.Data.Services.Common.EntitySetAttribute(
"TradeHeaders"
)]
[global::System.Data.Services.Common.DataServiceKeyAttribute(
"FactTradeHeaderIdentifier"
)]
public
partial
class
TradeHeaderModel : global::System.ComponentModel.INotifyPropertyChanged
{
/// <summary>
/// There are no comments for Property FactTradeHeaderIdentifier in the schema.
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(
"System.Data.Services.Design"
,
"1.0.0"
)]
public
int
FactTradeHeaderIdentifier
{
get
{
return
this
._FactTradeHeaderIdentifier;
}
set
{
this
.OnFactTradeHeaderIdentifierChanging(value);
this
._FactTradeHeaderIdentifier = value;
this
.OnFactTradeHeaderIdentifierChanged();
this
.OnPropertyChanged(
"FactTradeHeaderIdentifier"
);
}
}
...
}
When loading data into the QueryableDataServiceCollectionView
var ds =
new
QueryableDataServiceCollectionView<TradeHeaderModel>(apiService.Container, apiService.Container.TradeHeaders);
I get this error:
System.ArgumentException: The DataServiceCollection to be tracked must contain entity typed elements with at least one key property. The element type 'Edft.Regulatory.Tracker.Presentation.CrossCutting.ServiceClient.ApiODataService.TradeHeaderModel' does not have any key property.
at System.Data.Services.Client.DataServiceCollection`1.StartTracking(DataServiceContext context, IEnumerable`1 items, String entitySet, Func`2 entityChanged, Func`2 collectionChanged)
at System.Data.Services.Client.DataServiceCollection`1..ctor(DataServiceContext context)
at Telerik.Windows.Controls.DataServices.DataServiceCollection`1..ctor(DataServiceContext context)
at Telerik.Windows.Data.QueryableDataServiceCollectionView`1..ctor(DataServiceContext dataServiceContext, DataServiceQuery`1 dataServiceQuery)
at Edft.Regulatory.Tracker.Presentation.Modules.Dashboard.ViewModels.TradeHeaderViewModel.<LoadDataAsync>d__1f.MoveNext() in d:\TFS\Regulatory\Tracker\Dev\Codebase\Edft.Regulatory.Tracker.Presentation.Modules.Dashboard\ViewModels\TradeHeaderViewModel.cs:line 272
Note that the DataServiceKeyAttribute is defined on both client and server models.
Any ideas?
Edit: Using the Telerik.Windows.Controls.DataServices50 version
Thanks,
Stevo
10 Answers, 1 is accepted
We simply use the standard Microsoft class DataServiceCollection as described here. We don't actually perform any of the WCF Data Services work -- we simply append Where and OrderBy's on the query -- that's it. Our component simply steps on top of the WCF Data Services stack and its only job is to append those Where and OrderBy clauses to the DataServiceQuery<T>. It does not do anything else -- all the job is done by the DataServiceContext, DataServiceCollection and DataServiceQuery.
From the stack trace I can see that it is the DataServiceCollection that complains about the query.
I have the following question. If you remove all Telerik components, can you successfully create and use such a DataServiceCollection<T> to make queries as described in this article?
Please, let me know what the results are.
Rossen Hristov
Telerik
Learn what features your users use (or don't use) in your application. Know your audience. Target it better. Develop wisely.
Sign up for Free application insights >>
I've tried to use the DataServiceCollection<T> directly, passing it DataServiceQuery<T> appended with same .Where(...) (which makes it IQueryable<T>) and that works fine.
However that strips away the possibility of doing server side sorting and paging.
Regards,
Stevo
I don't really think that this exception is caused by our components. In fact I am sure that it is not by reading the stack trace you posted. The exception is thrown the very moment we try to create this DataServiceCollection with the supplied arguments.
Here is what we do in our very thin wrapper:
var something = new DataServiceCollection<TradeHeaderModel>(apiService.Container);
Regards,
Rossen Hristov
Telerik
Learn what features your users use (or don't use) in your application. Know your audience. Target it better. Develop wisely.
Sign up for Free application insights >>
just tried:
var data = new DataServiceCollection<TradeHeaderModel>(apiService.Container.TradeHeaders); // Works Fine
var data = new QueryableDataServiceCollectionView<TradeHeaderModel>(apiService.Container, apiService.Container.TradeHeaders); //Crashes complaining about the missing key
Note that what you've suggested
var something = new DataServiceCollection<TradeHeaderModel>(apiService.Container);
Does not do anything, since container does not have any data as such
Regards,
Stevo
The thing I suggested is exactly what we do in our constructor. Here is an excerpt from our souce code:
this.wcfDataServiceCollection = new WCF.DataServiceCollection<TEntity>(this.context);
Where TEntity in this case is TradeHeaderModel and this.context is the instance of DataServiceContext<T> that you supply in the constructor. It learns about the type from the generic argument as we will see later.
Once we send the query to the server and receive the response we call DataServiceCollection.Load method to load all received entities in the DataServiceCollection. We simply follow what is written in MSDN here.
Anyway, here is the stacktrace again:
System.ArgumentException: The DataServiceCollection to be tracked must contain entity typed elements with at least one key property. The element type 'Edft.Regulatory.Tracker.Presentation.CrossCutting.ServiceClient.ApiODataService.TradeHeaderModel' does not have any key property.
at System.Data.Services.Client.DataServiceCollection`1.StartTracking(DataServiceContext context, IEnumerable`1 items, String entitySet, Func`2 entityChanged, Func`2 collectionChanged)
at System.Data.Services.Client.DataServiceCollection`1..ctor(DataServiceContext context)
at Telerik.Windows.Controls.DataServices.DataServiceCollection`1..ctor(DataServiceContext context)
at Telerik.Windows.Data.QueryableDataServiceCollectionView`1..ctor(DataServiceContext dataServiceContext, DataServiceQuery`1 dataServiceQuery)
at Edft.Regulatory.Tracker.Presentation.Modules.Dashboard.ViewModels.TradeHeaderViewModel.<LoadDataAsync>d__1f.MoveNext() in d:\TFS\Regulatory\Tracker\Dev\Codebase\Edft.Regulatory.Tracker.Presentation.Modules.Dashboard\ViewModels\TradeHeaderViewModel.cs:line 272
The things in yellow is the souce code I pasted above. We simply create a new instance of a generic System.Data.Services.Client.DataServiceCollection<TradeHeaderModel> by passing your context to the constructor. That is all. I reflected their assembly and here is the constructor:
public
DataServiceCollection(DataServiceContext context, IEnumerable<T> items, TrackingMode trackingMode,
string
entitySetName, Func<EntityChangedParams,
bool
> entityChangedCallback, Func<EntityCollectionChangedParams,
bool
> collectionChangedCallback)
{
if
(trackingMode != TrackingMode.AutoChangeTracking)
{
if
(items !=
null
)
{
this
.Load(items);
}
}
else
{
if
(context ==
null
)
{
if
(items !=
null
)
{
context = DataServiceCollection<T>.GetContextFromItems(items);
}
else
{
this
.trackingOnLoad =
true
;
this
.entitySetName = entitySetName;
this
.entityChangedCallback = entityChangedCallback;
this
.collectionChangedCallback = collectionChangedCallback;
}
}
if
(!
this
.trackingOnLoad)
{
if
(items !=
null
)
{
DataServiceCollection<T>.ValidateIteratorParameter(items);
}
this
.StartTracking(context, items, entitySetName, entityChangedCallback, collectionChangedCallback);
return
;
}
}
}
And here is the StartTracking method I decompiled:
private
void
StartTracking(DataServiceContext context, IEnumerable<T> items,
string
entitySet, Func<EntityChangedParams,
bool
> entityChanged, Func<EntityCollectionChangedParams,
bool
> collectionChanged)
{
if
(BindingEntityInfo.IsEntityType(
typeof
(T), context.Model))
{
this
.observer =
new
BindingObserver(context, entityChanged, collectionChanged);
if
(items !=
null
)
{
try
{
this
.InternalLoadCollection(items);
}
catch
{
this
.observer =
null
;
throw
;
}
}
this
.observer.StartTracking<T>(
this
, entitySet);
this
.rootCollection =
true
;
return
;
}
else
{
throw
new
ArgumentException(Strings.DataBinding_DataServiceCollectionArgumentMustHaveEntityType(
typeof
(T)));
}
}
For some mystic reason the thing that tracks entities thinks that your class TradeHeaderModel does not have at least one Key property or that it is not an Entity at all. This is really beyond me. If you try with a dummy demo with AdventureWorks or Northwind you will see that all goes well, so there must be something funny in this TradeHeaderModel class but I do not know what it is.
I really have no idea why the System.Data.Services.Client.DataServiceCollection does not like your TradeHeaderModel class.
So basically you are saying that if you write this exact line somewhere in your project:
var something = new System.Data.Services.Client
DataServiceCollection<TradeHeaderModel>(<<your DataServiceContext instance here>>);
and it does not blow?
That is very weird since we do this exact same thing and it blows up as you can see.
Anyway, unless I am able to reproduce this locally, I can't really guess why Microsoft's System.Data.Services.Client.DataServiceCollection blows up.
Also, please make sure that you are using WCF Data Services 5.5.0 or above which is the out-of-band version that our assembly Telerik.Windows.Controls.DataServices50.dll for .NET 4.5. Only the .NET 4.5 version of our assembly can target WCF Data Services 5 and above since Microsoft decided to start releasing WCF Data Services out-of-band and we can't really create a build for each of their version that are distributed via NuGet.
Another thing -- if you don't do any CRUD operations, you don't really need our component - you can directly bind RadGridView to an IQueryable (i.e. the DataServiceQuery<T> on your context) and it will do all of its stuff on this IQueryable. This is described here.
If you can help me reproduce this locally I will be more than glad to take a look. With the information I currently have my hands are kind of tied. Regards,
Rossen Hristov
Telerik
Learn what features your users use (or don't use) in your application. Know your audience. Target it better. Develop wisely.
Sign up for Free application insights >>
>>The thing I suggested is exactly what we do in our constructor. Here is an excerpt from our souce code:
this.wcfDataServiceCollection = new WCF.DataServiceCollection<TEntity>(this.context);
Where TEntity in this case is TradeHeaderModel and this.context is the instance of DataServiceContext<T> that you supply in the constructor. It learns about the type from the generic argument as we will see later.
Once we send the query to the server and receive the response we call DataServiceCollection.Load method to load all received entities in the DataServiceCollection. We simply follow what is written in MSDN here.
Looking at MSDN, I don't understand why are you guys using that constructor. It's not meant for loading from server. http://msdn.microsoft.com/en-us/library/ee652763.aspx
You must be loading data somehow differently (and that's where I suppose the problem happens). All the MSDN examples show passing it the actual query:
http://msdn.microsoft.com/en-us/library/ee373844.aspx
http://msdn.microsoft.com/en-us/library/ee474331.aspx
http://msdn.microsoft.com/en-us/library/ee373846.aspx
>>var something = new System.Data.Services.Client
DataServiceCollection<TradeHeaderModel>(<<your DataServiceContext instance here>>);
and it does not blow?
That does not do anything, and it's not mean to do http://msdn.microsoft.com/en-us/library/ee652763.aspx
>>Another thing -- if you don't do any CRUD operations, you don't really need our component - you can directly bind RadGridView to an IQueryable (i.e. the DataServiceQuery<T> on your context) and it will do all of its stuff on this IQueryable. This is described here.
It's runs the queries on the UI thread, which we can't have for obvious reasons.
Regards,
Stevo
The process is quite simple actually. We create an empty DataServiceCollection<T> in the very beginning. Then each time a load is requested for some reason we basically do the following. We call DataServiceContext.BeginExecute by passing the required query. This kicks off an async operation. After a while data comes back to the client. We then call context.EndExecute and obtain the entities. We then stick all of those entities in the DataServiceCollection by callind its Load method so it starts tracking them. Here is the complete source code of our thin wrapper class called Telerik.Windows.Controls.DataServices.DataServiceCollection. Note that while the name is the same, this is our class and not the System.Data.Services.Client.DataServiceCollection, which is the class we wrap and is called wcfDataServiceCollection in the source code:
using
System;
using
System.Collections;
using
System.ComponentModel;
using
System.Threading;
using
System.Windows;
using
System.Diagnostics;
using
System.Data.Services.Client;
using
System.Collections.Specialized;
using
System.Linq;
using
System.Net;
using
Telerik.Windows.Data;
using
WCF = System.Data.Services.Client;
namespace
Telerik.Windows.Controls.DataServices
{
/// <summary>
/// DataServiceCollection.
/// </summary>
internal
class
DataServiceCollection<TEntity> :
ObservableItemCollection<TEntity>,
INotifyPropertyChanged
where TEntity :
class
, INotifyPropertyChanged
{
private
readonly
DataServiceContext context;
private
readonly
WCF.DataServiceCollection<TEntity> wcfDataServiceCollection;
public
event
EventHandler<LoadOperationCompletedEventArgs<TEntity>> LoadCompleted;
private
SynchronizationContext syncContext;
private
IAsyncResult currentLoadOperation;
private
bool
cancelledRequest;
private
bool
ignoreCollectionChangedOverride;
private
int
totalItemCount = -1;
private
bool
isLoading;
private
bool
hasChanges;
/// <summary>
/// Gets the total item count.
/// </summary>
/// <value>The total item count.</value>
public
int
TotalItemCount
{
get
{
return
this
.totalItemCount;
}
private
set
{
if
(
this
.totalItemCount != value)
{
this
.totalItemCount = value;
this
.OnPropertyChanged(
"TotalItemCount"
);
}
}
}
public
bool
IsLoading
{
get
{
return
this
.isLoading;
}
private
set
{
if
(
this
.isLoading != value)
{
this
.isLoading = value;
this
.OnPropertyChanged(
"IsLoading"
);
}
}
}
public
bool
HasChanges
{
get
{
return
this
.hasChanges;
}
private
set
{
if
(
this
.hasChanges != value)
{
this
.hasChanges = value;
this
.OnPropertyChanged(
"HasChanges"
);
}
}
}
/// <summary>
/// Initializes a new instance of the DataServiceCollection class.
/// </summary>
/// <param name="context">The context.</param>
public
DataServiceCollection(DataServiceContext context)
{
if
(context ==
null
)
{
throw
new
ArgumentNullException(
"context"
);
}
this
.context = context;
this
.wcfDataServiceCollection =
new
WCF.DataServiceCollection<TEntity>(
this
.context);
}
public
void
SetSynchronizationContext(SynchronizationContext synchronizationContext)
{
this
.syncContext = synchronizationContext;
}
public
void
Load(LoadContext loadContext)
{
this
.wcfDataServiceCollection.Clear();
var query = loadContext.DataServiceQuery;
var queryRequestUri = query.RequestUri;
this
.IsLoading =
true
;
// Any other option will break the inner logic.
if
(
this
.context.MergeOption != MergeOption.OverwriteChanges)
{
throw
new
InvalidOperationException(
"DataServiceContext.MergeOption can only be MergeOption.OverwriteChanges."
);
}
this
.currentLoadOperation =
this
.context.BeginExecute<TEntity>(queryRequestUri
,
this
.OnResultsReceived
, loadContext);
}
public
void
CancelLoad()
{
if
(
this
.currentLoadOperation !=
null
&& !
this
.currentLoadOperation.IsCompleted)
{
this
.cancelledRequest =
true
;
try
{
this
.context.CancelRequest(
this
.currentLoadOperation);
}
catch
(WebException)
{
}
}
}
/// <summary>
/// Called when the results arrive asynchronously from the server.
/// </summary>
/// <remarks>
/// Since there is no guarantee that this method will be invoked on the UI thread,
/// we have to marshal the response operation back to the main application thread
/// (the UI thread). Code that accesses entities, accesses the DataServiceContext or
/// enumerates results of LINQ queries needs to happen on the UI thread.
/// </remarks>
/// <param name="result">The result.</param>
private
void
OnResultsReceived(IAsyncResult result)
{
this
.syncContext.Post(
delegate
(
object
state)
{
this
.SynchronizeContextLoadCompleted(result);
},
null
);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Microsoft.Design"
,
"CA1031:DoNotCatchGeneralExceptionTypes"
)]
private
void
SynchronizeContextLoadCompleted(IAsyncResult result)
{
if
(
this
.cancelledRequest)
{
this
.cancelledRequest =
false
;
this
.currentLoadOperation =
null
;
this
.IsLoading =
false
;
var args =
new
LoadOperationCompletedEventArgs<TEntity>(Enumerable.Empty<TEntity>()
,
null
, (LoadContext)result.AsyncState
,
true
,
null
, 0);
this
.OnLoadCompleted(args);
}
else
{
QueryOperationResponse<TEntity> response =
null
;
try
{
response = (QueryOperationResponse<TEntity>)
this
.context.EndExecute<TEntity>(result);
}
catch
(Exception exception)
{
this
.IsLoading =
false
;
var args =
new
LoadOperationCompletedEventArgs<TEntity>(Enumerable.Empty<TEntity>()
, response
, (LoadContext)result.AsyncState
,
false
, exception
, 0);
this
.OnLoadCompleted(args);
return
;
}
if
(response !=
null
)
{
this
.ProcessResponse(response, (LoadContext)result.AsyncState);
}
}
}
private
void
ProcessResponse(QueryOperationResponse<TEntity> response, LoadContext loadContext)
{
//// Load the entities that have just arrived in the "state"
//// DataServiceCollection so it can start "tracking" them.
//// response is an IEnumerable<T> and contains the entities
this
.wcfDataServiceCollection.Load(response);
var continuation = response.GetContinuation();
if
(continuation !=
null
)
{
// Send a request for the next server page.
this
.currentLoadOperation =
this
.context.BeginExecute<TEntity>(continuation
,
this
.OnResultsReceived, loadContext);
}
else
{
this
.IsLoading =
false
;
// This was the last page, so copy all the entities to "this".
this
.SuspendNotifications();
this
.ignoreCollectionChangedOverride =
true
;
this
.Clear();
this
.AddRange(
this
.wcfDataServiceCollection);
this
.ignoreCollectionChangedOverride =
false
;
try
{
this
.TotalItemCount = (
int
)response.TotalCount;
}
catch
(InvalidOperationException)
{
// The server refuses to tell us the total count for some reason.
// Most probably the DataServiceBehavior has its
// AcceptCountRequests set to false.
// Set the TotalItemCount to be as much as we know.
// Nothing better that we can do. Bad server, bad!
this
.TotalItemCount =
this
.Count;
}
this
.ResumeNotifications();
}
var args =
new
LoadOperationCompletedEventArgs<TEntity>(
this
.wcfDataServiceCollection
, response
, loadContext
,
false
,
null
,
this
.TotalItemCount);
this
.OnLoadCompleted(args);
}
protected
override
void
OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base
.OnCollectionChanged(e);
if
(
this
.ignoreCollectionChangedOverride)
{
return
;
}
switch
(e.Action)
{
case
NotifyCollectionChangedAction.Add:
foreach
(TEntity item
in
e.NewItems)
{
this
.wcfDataServiceCollection.Add(item);
this
.TotalItemCount++;
}
break
;
case
NotifyCollectionChangedAction.Remove:
foreach
(TEntity item
in
e.OldItems)
{
this
.wcfDataServiceCollection.Remove(item);
this
.TotalItemCount--;
}
break
;
case
NotifyCollectionChangedAction.Replace:
break
;
case
NotifyCollectionChangedAction.Reset:
break
;
default
:
break
;
}
this
.RefreshHasChanges();
}
protected
override
void
OnItemChanged(ItemChangedEventArgs<TEntity> e)
{
base
.OnItemChanged(e);
this
.RefreshHasChanges();
}
public
void
RefreshHasChanges()
{
this
.HasChanges =
this
.context.HasChanges();
}
private
void
OnPropertyChanged(
string
propertyName)
{
this
.OnPropertyChanged(
new
PropertyChangedEventArgs(propertyName));
}
protected
virtual
void
OnLoadCompleted(LoadOperationCompletedEventArgs<TEntity> args)
{
var handler =
this
.LoadCompleted;
if
(handler !=
null
)
{
handler(
this
, args);
}
}
}
}
Yes, creating a new empty instance of DataServiceCollection<T>(context) does not do anything immediately (at this exact moment) but this is where your exception occurs, which is why I asked you several times for to simply try this line of code outside our component, which you for some reason are refusing to do. I can't really fix an exception that is thrown by WCF Data Services when this line is written:
var sth = new DataServiceCollection<T>(context);
This is one of the constructors that Microsoft provides (so it has a point after all) and our software happens to use this constructor for the reasons I explained above. This constructor thows an exception. All we pass to this constructor are the two thing provied by you -- the generic type TradeHeaderMode and the DataServiceContext that you have provided. We don't do anything in between.
This is why I asked you several times to just execute this line:
var sth = new DataServiceCollection<T>(context);
And tell me what the results are. Up until now I have not received an answer.
If you can provide a runnable sample project which I can debug locally I might be able to help you. But unless I have that I really don't know what else I can do about this issue.
Regards, Rossen Hristov
Telerik
Learn what features your users use (or don't use) in your application. Know your audience. Target it better. Develop wisely.
Sign up for Free application insights >>
I've started drilling into the problem again, and it just started working. I'm not sure what has changed in the meantime, I remember updating Telerik dlls to latest version, maybe some other references have changed and the problem disappeared.
Thanks for your support,
Stevo
Hi Rossen,
We are using QueryableDataServiceCollectionView with RadGridView and autoLoad set to true.
While loading the gridview, in the fiddler we are observing one main request with all the required filters on the query and many individual requests to the server (one for each item). Is this expected behaviour?
By doing this.wcfDataServiceCollection.Load(response) in the ProcessResponse(QueryOperationResponse<TEntity> response, LoadContext loadContext), it is triggering multiple requests to the server as the number of items. Shouldn't it just load from the response obtained?
As we are fetching the response (all the required items) from the DataServiceQuery, why are we again requesting the server for all the individual items?
Hello Mohan,
Thank you for the shared information.
My current understanding is that you are filtering the RadGridView, which is populated with the help of a QueryableDataServiceCollectionView and you are observing a request in Fiddler for each of the filtered items. Feel free to correct me, if I am wrong and elaborate a bit on the scenario.
I tested this on my end, however I was not able to replicate the same behavior. That is why I am attaching the sample project that I used for testing. Can you check it out and see how it differs from the setup on your end? Should you need any further assistance, can you modify the project in order to demonstrate your scenario and send it over in a new support ticket (since project files cannot be attached to forum posts)? This will hopefully allow me to investigate the behavior and further assist you.
Regards,
Vladimir Stoyanov
Progress Telerik
Our thoughts here at Progress are with those affected by the outbreak.