ReactiveUI - DynamicData switching between streams to a single ReadOnlyObservableCollection - android-recyclerview

I have a ReadOnlyObservableCollection<ItemViewModel> that I want to toggle between showing all the items and limiting to top 2. I have the following sample code that works.
public class MainViewModel : ReactiveObject
{
private readonly SourceCache<ItemViewModel, string> _sourceCache = new SourceCache<ItemViewModel, string>(x => x.Text);
private readonly BehaviorSubject<bool> _toggleSubject = new BehaviorSubject<bool>(false);
public ReadOnlyObservableCollection<ItemViewModel> ObsCollection;
public ReactiveCommand<Unit, Unit> ToggleCommand { get; }
public MainViewModel()
{
ToggleCommand = ReactiveCommand.Create(() =>
{
_toggleSubject.OnNext(!_toggleSubject.Value);
});
_sourceCache.AddOrUpdate(new ItemViewModel() { Text = "a" });
_sourceCache.AddOrUpdate(new ItemViewModel() { Text = "c" });
_sourceCache.AddOrUpdate(new ItemViewModel() { Text = "b" });
IComparer<ItemViewModel> comparer = Comparer<ItemViewModel>.Create((x, y) => x.Text.CompareTo(y.Text));
var observableChanges = _sourceCache.Connect();
var limitedStream = _toggleSubject.Where(x => !x).Select(_ => observableChanges.Top(comparer: comparer, size: 2));
var fullStream = _toggleSubject.Where(x => x).Select(_ => observableChanges.Sort(comparer));
Observable.Merge(limitedStream, fullStream)
.Switch()
.Bind(out ObsCollection)
.Subscribe();
}
}
Using the ToggleCommand, the ObsCollection emits the expected result. But when I try to observe the ChangeSets (ViewModel.ObsCollection.ToObservableChangeSet()) in a ReactiveRecyclerViewAdapter<ItemViewModel> the wrong results are displayed on toggle. Instead of displaying a,b,c the list displays a,b,b. Debuging through the implementation of ReactiveRecyclerViewAdapter I see that the change.Range.Index is set to -1 instead of what I expected 0. I think that is why the last value is not updated.
case ListChangeReason.AddRange:
NotifyItemRangeInserted(change.Range.Index, change.Range.Count);
break;
case ListChangeReason.RemoveRange:
case ListChangeReason.Clear:
NotifyItemRangeRemoved(change.Range.Index, change.Range.Count);
break;
Why would the any change start at index -1? Is this type of implementation valid. I have the full project to reproduce this.

Combining change sets in dynamic data using the rx merge operator will not work. This is because there is no way for subsequent operators to understand that the changes come from different sources which leads to unpredictable results.
Instead dynamic data provides it's own operators which enables various means to join changes sets. Examples of this are the And and Or operators which apply a logical join across the entire collection. However in this case we are swapping the result out so correct way is to use the Dynamic Data overload of Switch which unloads previous results and loads the new results when the toggle is fired.
var toggled = _toggleSubject
//when the toggle subject changes select top values or all accordingly
.Select(b => b ? observableChanges.Top(comparer: comparer, size: 2) : observableChanges.Sort(comparer))
//this is a dynamic data overload of switch which flattens the nested observable
.Switch();

Related

how to use firestore query result in another query. Kotlin

Tell me please. Here is the base:
"orders" inside this base there are fields ID, number, address, and so on and there is a collection of "carpets" inside the base of carpets there are also different fields, including the field cost (that is, the cost of cleaning this carpet).
now QUESTION:
how to calculate the total cost and write the result in the "orders" database field?
in general it is interesting how to implement it. How to make such queries so that later the result obtained is already written in a different field?
A good approach for a summary field is to define a property on the parent document ("order", in the OP case) and code a write-trigger on the child collection ("order/carpets").
The trigger's job is to determine what sort of write has taken place on the collection and update the parent doc's prop accordingly.
Code something like (very roughly like) the following in your cloud functions folder...
// when carpets are written, update their parent order's "ordersTotal" prop
exports.didUpdateCarpets = functions.firestore.document('orders/{orderId}/carpets').onWrite(async (change, context) => {
const ref = db.collection('orders').doc(orderId);
try {
await runTransaction(db, async (transaction) => {
const doc = await transaction.get(ref);
let ordersTotal = doc.data().ordersTotal;
// modify orderTotal based on the trigger params
const before = change.before.exists ? change.before.data() : null;
const after = change.after.exists ? change.after.data() : null;
if (!before) ordersTotal += after.cost; // created
else if (!after) ordersTotal -= before.cost; // deleted
else ordersTotal += after.cost - before.cost; // modified
transaction.update(ref, { ordersTotal });
});
} catch (e) {
console.log("Transaction failed: ", e);
}
});

PagedListAdapter jumps to beginning of the list on receiving new PagedList

I'm using Paging Library to load data from network using ItemKeyedDataSource. After fetching items user can edit them, this updates are done inside in Memory cache (no database like Room is used).
Now since the PagedList itself cannot be updated (discussed here) I have to recreate PagedList and pass it to the PagedListAdapter.
The update itself is no problem but after updating the recyclerView with the new PagedList, the list jumps to the beginning of the list destroying previous scroll position. Is there anyway to update PagedList while keeping scroll position (like how it works with Room)?
DataSource is implemented this way:
public class MentionKeyedDataSource extends ItemKeyedDataSource<Long, Mention> {
private Repository repository;
...
private List<Mention> cachedItems;
public MentionKeyedDataSource(Repository repository, ..., List<Mention> cachedItems){
super();
this.repository = repository;
this.teamId = teamId;
this.inboxId = inboxId;
this.filter = filter;
this.cachedItems = new ArrayList<>(cachedItems);
}
#Override
public void loadInitial(#NonNull LoadInitialParams<Long> params, final #NonNull ItemKeyedDataSource.LoadInitialCallback<Mention> callback) {
Observable.just(cachedItems)
.filter(() -> return cachedItems != null && !cachedItems.isEmpty())
.switchIfEmpty(repository.getItems(..., params.requestedLoadSize).map(...))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(response -> callback.onResult(response.data.list));
}
#Override
public void loadAfter(#NonNull LoadParams<Long> params, final #NonNull ItemKeyedDataSource.LoadCallback<Mention> callback) {
repository.getOlderItems(..., params.key, params.requestedLoadSize)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(response -> callback.onResult(response.data.list));
}
#Override
public void loadBefore(#NonNull LoadParams<Long> params, final #NonNull ItemKeyedDataSource.LoadCallback<Mention> callback) {
repository.getNewerItems(..., params.key, params.requestedLoadSize)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(response -> callback.onResult(response.data.list));
}
#NonNull
#Override
public Long getKey(#NonNull Mention item) {
return item.id;
}
}
The PagedList created like this:
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(PAGE_SIZE)
.setInitialLoadSizeHint(preFetchedItems != null && !preFetchedItems.isEmpty()
? preFetchedItems.size()
: PAGE_SIZE * 2
).build();
pagedMentionsList = new PagedList.Builder<>(new MentionKeyedDataSource(mRepository, team.id, inbox.id, mCurrentFilter, preFetchedItems)
, config)
.setFetchExecutor(ApplicationThreadPool.getBackgroundThreadExecutor())
.setNotifyExecutor(ApplicationThreadPool.getUIThreadExecutor())
.build();
The PagedListAdapter is created like this:
public class ItemAdapter extends PagedListAdapter<Item, ItemAdapter.ItemHolder> { //Adapter from google guide, Nothing special here.. }
mAdapter = new ItemAdapter(new DiffUtil.ItemCallback<Mention>() {
#Override
public boolean areItemsTheSame(Item oldItem, Item newItem) {
return oldItem.id == newItem.id;
}
#Override
public boolean areContentsTheSame(Item oldItem, Item newItem) {
return oldItem.equals(newItem);
}
});
, and updated like this:
mAdapter.submitList(pagedList);
You should use a blocking call on your observable. If you don't submit the result in the same thread as loadInitial, loadAfter or loadBefore, what happens is that the adapter will compute the diff of the existing list items against an empty list first, and then against the newly loaded items. So effectively it's as if all items were deleted and then inserted again, that is why the list seems to jump to the beginning.
You're not using androidx.paging.ItemKeyedDataSource.LoadInitialParams#requestedInitialKey in your implementation of loadInitial, and I think you should be.
I took a look at another implementation of ItemKeyedDataSource, the one used by autogenerated Room DAO code: LimitOffsetDataSource. Its implementation of loadInitial contains (Apache 2.0 licensed code follows):
// bound the size requested, based on known count
final int firstLoadPosition = computeInitialLoadPosition(params, totalCount);
final int firstLoadSize = computeInitialLoadSize(params, firstLoadPosition, totalCount);
... where those functions do something with params.requestedStartPosition, params.requestedLoadSize and params.pageSize.
So what's going wrong?
Whenever you pass a new PagedList, you need to make sure that it contains the elements that the user is currently scrolled to. Otherwise, your PagedListAdapter will treat this as a removal of these elements. Then, later, when your loadAfter or loadBefore items load those elements, it will treat them as a subsequent insertion of these elements. You need to avoid doing this removal and insertion of any visible items. Since it sounds like you're scrolling to the top, maybe you're accidentally removing all items and inserting them all.
The way I think this works when using Room with PagedLists is:
The database is updated.
A Room observer invalidates the data source.
The PagedListAdapter code spots the invalidation and uses the factory to create a new data source, and calls loadInitial with the params.requestedStartPosition set to a visible element.
A new PagedList is provided to the PagedListAdapter, who runs the diff checking code to see what's actually changed. Usually, nothing has changed to what's visible, but maybe an element has been inserted, changed or removed. Everything outside the initial load is treated as being removed - this shouldn't be noticeable in the UI.
When scrolling, the PagedListAdapter code can spot that new items need to be loaded, and call loadBefore or loadAfter.
When these complete, an entire new PagedList is provided to the PagedListAdapter, who runs the diff checking code to see what's actually changed. Usually - just an insertion.
I'm not sure how that corresponds to what you're trying to do, but maybe that helps? Whenever you provide a new PagedList, it will be diffed against the previous one, and you want to make sure that there's no spurious insertions or deletions, or it can get really confused.
Other ideas
I've also seen issues where PAGE_SIZE is not big enough. The docs recommend several times the maximum number of elements that can be visible at a time.
This also happens when DiffUtil.ItemCallback is not correctly implemented. And by correct implementation I mean, you should properly check whether the oldItem and newItem are same or not and accordingly return true or false from areItemsTheSame() and areContentsTheSame() methods.
For example, if I always return false from both of these methods like:
DiffUtil.ItemCallback<Mention>() {
#Override
public boolean areItemsTheSame(Item oldItem, Item newItem) {
return false;
}
#Override
public boolean areContentsTheSame(Item oldItem, Item newItem) {
return false;
}
}
The library thinks that all the items are new therefore it jumps to the top to display all the new items.
So make sure you carefully check the oldItem and newItem and properly return true or false based on your comparisons

Returning distinct data for a dropdownlist box with selectlistItem

I have a field in my database with duplicates. I want to use it in a dropdown list, which has to return distinct data.
Here is the method that I created to do this:
public IEnumerable<SelectListItem> GetBranches(string username)
{
using (var objData = new BranchEntities())
{
IEnumerable<SelectListItem> objdataresult = objData.ABC_USER.Select(c => new SelectListItem
{
Value = c.BRANCH_CODE.ToString(),
Text = c.BRANCH_CODE
}).Distinct(new Reuseablecomp.SelectListItemComparer());
return objdataresult;
}
}
Here is the class I am using:
public static class Reuseablecomp
{
public class SelectListItemComparer : IEqualityComparer<SelectListItem>
{
public bool Equals(SelectListItem x, SelectListItem y)
{
return x.Text == y.Text && x.Value == y.Value;
}
public int GetHashCode(SelectListItem item)
{
int hashText = item.Text == null ? 0 : item.Text.GetHashCode();
int hashValue = item.Value == null ? 0 : item.Value.GetHashCode();
return hashText ^ hashValue;
}
}
}
Nothing is returned and I get the error below. When I try a basic query without Distinct, everything works fine.
{"The operation cannot be completed because the DbContext has been disposed."}
System.Exception {System.InvalidOperationException}
Inner exception = null
How can I return distinct data for my dropdown?
Technically, your problem can be solved simply by appending .ToList() after your Distinct(...) call. The problem is that queries are evaluated JIT (just in time). In other words, until the actual data the query represents is needed, the query is not actually sent to the database. Calling ToList is one such thing that requires the actual data, and therefore will cause the query to be evaluated immediately.
However, the root cause of your problem is that you are doing this within a using statement. When the method exits, the query has not yet been evaluated, but you have now disposed of your context. Therefore, when it comes time to actually evaluate that query, there's no context to do it with and you get that exception. You should really never use a database context in conjuction with using. It's just a recipe for disaster. Your context should ideally be request-scoped and you should use dependency injection to feed it to whatever objects or methods need it.
Also, for what it's worth, you can simply move your Distinct call to before your Select and you won't need a custom IEqualityComparer any more. For example:
var objdataresult = objData.ABC_USER.Distinct().Select(c => new SelectListItem
{
Value = c.BRANCH_CODE.ToString(),
Text = c.BRANCH_CODE
});
Order of ops does matter here. Calling Distinct first includes it as part of the query to the database, but calling it after, as you're doing, runs it on the in-memory collection, once evaluated. The latter requires, then, custom logic to determine what constitutes distinct items in an IEnumerable<SelectListItem>, which is obviously not necessary for the database query version.

Get all SmartForm items from Ektron 9 in a Taxonomy

I'm using Ektron CMS version 9.0
I have smart form content which is allocated to taxonomies e.g. I might have five smart form content items (all of same) type allocated to a taxonomy, and another three to a different taxonomy:
I need to get all content of a smart form type from a taxonomy:
public IEnumerable<T> GetListOfSmartFormFromTaxonomy<T>(long taxonomyId, bool isRecursive) where T : class
{
// TODO
}
What I have working, based on links below, is this:
public IEnumerable<TaxonomyItemData> GetListOfSmartFormFromTaxonomy(long taxonomyId)
{
TaxonomyItemCriteria criteria = new TaxonomyItemCriteria();
criteria.AddFilter(TaxonomyItemProperty.TaxonomyId, CriteriaFilterOperator.EqualTo, taxonomyId);
TaxonomyItemManager taxonomyItemManager = new TaxonomyItemManager();
List<TaxonomyItemData> taxonomyItemList = taxonomyItemManager.GetList(criteria);
return taxonomyItemList;
}
But this just gets the item's titles and ids, not the smart form data itself.
As an Ektron newbie, I don't know how to get all the items of one Smart Form type using only one call (instead of looping through each item and fetching it by ID which is not efficient)
What have I missed? I am working on this actively today and will post my findings here.
References used so far:
http://reference.ektron.com/developer/framework/Organization/TaxonomyItemManager/GetList.asp
Ektron taxonomy and library items (in v9)
EDIT
Posted my just-got-it-working solution below as an fyi and awarded closest answer as accepted. Thanks everyone for your help. Please chime in with any improvements ;)
I'd recommend using the ContentTaxonomyCriteria with the ContentManager.
long smartFormId = 42;
long taxonomyId = 127;
bool isRecursive = true;
var cm = new ContentManager();
var taxonomyCriteria = new ContentTaxonomyCriteria();
taxonomyCriteria.AddFilter(ContentProperty.XmlConfigurationId, CriteriaFilterOperator.EqualTo, smartFormId);
taxonomyCriteria.AddFilter(taxonomyId, isRecursive);
var content = cm.GetList(taxonomyCriteria);
UPDATE
The ContentData object has a property called XmlConfiguration. When the content is based on a smartform, this property will be non-null and have a positive (non-zero) Id: content[0].XmlConfiguration.Id for example.
I often add an Extension Method to my code that will tell me whether a given ContentData is based on a smart form:
public static class ContentDataExtensions
{
public static bool IsSmartFormContent(this ContentData content)
{
return content != null && content.XmlConfiguration != null && content.XmlConfiguration.Id > 0;
}
}
That way I can take a content (or list of content) and check it very quickly in code to see if it's based on a smartform or not:
foreach (var contentData in contentList)
{
if (contentData.IsSmartFormContent())
{
// Do smart-form stuff here...
}
}
Of course, if your content is coming from the framework api and you used a criteria object that is selecting based on a specific XmlConfigurationId, then in theory you wouldn't have to use that, but it still comes in handy quite often.
I'm not quite sure I understand your organizational structure, but you do have the ability to do your own sub clauses that select directly against the database.
In this case I wouldn't use the TaxonomyItemManager, I would use the ContentManager with a special criteria:
ContentManager cApi = new ContentManager();
var criteria = new ContentCriteria();
criteria.AddFilter(ContentProperty.Id, CriteriaFilterOperator.InSubClause, "select taxonomy_item_id where taxonomy_id = " + taxonomyId);
criteria.AddFilter(ContentProperty.XmlConfigurationId, CriteriaFilterOperator.EqualTo, smartformTypeId);
var yourContent = cApi.GetList(criteria);
That should do what you're asking for (grab the content specifically that is a member of a Taxonomy while only being of a specific SmartForm config). It's worth noting you don't need the second criteria piece (XmlConfigurationId) if your Taxonomy only contains that XmlConfiguration.
For Information, this is what I came up with. Noted Brian Oliver's comment on List but using patterns from other devs, can refactor later.
To clarify, we are creating classes from the XSDs generated from the smart forms, so have smart form types to play with. Your use may be simpler that ours.
public IEnumerable<T> GetListOfSmartFormFromTaxonomy<T>(long taxonomyId, bool isRecursive = false) where T : class
{
long smartFormId = GetSmartFormIdFromType(typeof(T));
// checks here for smartformid=0
ContentManager contentManager = new ContentManager();
ContentTaxonomyCriteria criteria = new ContentTaxonomyCriteria();
// Smart Form Type
criteria.AddFilter(ContentProperty.XmlConfigurationId, CriteriaFilterOperator.EqualTo, smartFormId);
// Taxonomy
criteria.AddFilter(taxonomyId, isRecursive);
List<ContentData> contentDataList = contentManager.GetList(criteria);
IEnumerable<T> smartFormList = ConvertToSmartFormList<T>(pressReleaseDataList);
return smartFormList;
}
private IEnumerable<T> ConvertToSmartFormList<T>(List<ContentData> contentDataList) where T : class
{
List<T> smartFormList = new List<T>();
if (contentDataList != null && contentDataList.Count > 0)
{
foreach (ContentData contentData in contentDataList)
{
if (contentData.IsSmartFormContent())
{
T smartForm = GetDeserializedContent<T>(contentData.Html);
if (smartForm != null)
{
PropertyInfo property = smartForm.GetType().GetProperty("ContentId");
if (property != null)
{
property.SetValue(smartForm, contentData.Id, null);
}
smartFormList.Add(smartForm);
}
}
}
}
return smartFormList;
}
private long GetSmartFormIdFromType(Type smartFormType)
{
SmartFormConfigurationManager manager = new SmartFormConfigurationManager();
SmartFormConfigurationCriteria criteria = new SmartFormConfigurationCriteria();
// Note: Smart Form Title must match the type's name, i.e. no spaces, for this to work
criteria.AddFilter(SmartFormConfigurationProperty.Title, CriteriaFilterOperator.EqualTo, smartFormType.Name);
List<SmartFormConfigurationData> configurationData = manager.GetList(criteria);
if (configurationData == null || configurationData.Count == 0)
{
return 0;
}
return configurationData.First().Id;
}

Unwrapping breeze Entity properties

I'm very new to breeze/knockout, but I'm 99% of the way to doing what I need to do. I'm using the Hot Towel template, and I'm successfully retrieving a list of items via breeze. The entity (ITBAL) is a database first Entity Framework entity. When I look at the JSON coming back in Fiddler, I see the correct data. The problem is that all of the properties of data.results are dependentobservables, and not the raw values themselves.
We have a custom grid control that is trying to display the data.results array. Because it is not expecting observables, it is simply displaying "function dependentobservable" instead of the value.
I tried to unwrap the object, but keep getting the circular reference error. I don't know why that is, since ITBAL isn't associated with anything.
The data as Fiddler reports it:
[{"$id":"1","$type":"WebUIHtml5HotTowel.Models.ITBAL, WebUIHtml5HotTowel","IBITNO":"A100 ","IBWHID":"1 ","IBITCL":"50","IBITSC":"3 ","IBSUSP":" ","IBVNNO":"100 ","IBPRLC":" ","IBSCLC":" ","IBCCCD":"P","IBPICD":" ","IBSAFL":"Y","IBSTCS":399.99000,"IBUSCS":0.00000,"IBAVCS":414.95214,"IBLCST":7.00000,"IBLCCC":20.0,"IBLCDT":110923.0,"IBLSCC":20.0,"IBLSDT":130111.0,"IBLXCC":19.0,"IBLXDT":990102.0,"IBMXO1":2100.000,"IBMXO2":0.000,"IBMXO3":0.000,"IBMNO1":5.000,"IBMNO2":0.000,"IBMNO3":0.000,"IBFOQ1":0.000,"IBFOQ2":0.000,"IBFOQ3":0.000,"IBOHQ1":327.000,"IBOHQ2":0.000,"IBOHQ3":0.000,"IBAQT1":1576.000,"IBAQT2":0.000,"IBAQT3":0.000,"IBBOQ1":50.000,"IBBOQ2":0.000,"IBBOQ3":0.000,"IBPOQ1":448.000,"IBPOQ2":0.000,"IBPOQ3":0.000,"IBIQT1":1446.000,"IBIQT2":0.000,"IBIQT3":0.000,"IBRMD1":10.000,"IBRMD2":0.000,"IBRMD3":0.000,"IBRYD1":10.000,"IBRYD2":0.000,"IBRYD3":0.000,"IBISM1":0.000,"IBISM2":0.000,"IBISM3":0.000,"IBISY1":0.000,"IBISY2":0.000,"IBISY3":0.000,"IBAMD1":0.000,"IBAMD2":0.000,"IBAMD3":0.000,"IBAYD1":0.000,"IBAYD2":0.000,"IBAYD3":0.000,"IBMMD1":0.000,"IBMMD2":0.000,"IBMMD3":0.000,"IBMYD1":0.000,"IBMYD2":0.000,"IBMYD3":0.000,"IBSMD1":1.0,"IBSMD2":0.0,"IBSMD3":0.0,"IBSYD1":1.0,"IBSYD2":0.0,"IBSYD3":0.0,"IBBLME":335.000,"IBBLYO":2680.000,"IBBLLY":1441.000,"IBNMTY":8.0,"IBNMLY":11.0,"IBQSMD":21.000,"IBQSYD":21.000,"IBQSLY":20.000,"IBISMD":16318.19,"IBISYD":16318.19,"IBISLY":45714.87,"IBCSMD":373.46,"IBCSYD":373.46,"IBCSLY":67.00,"IBDQMD":0.000,"IBDQYD":0.000,"IBDQLY":0.000,"IBDSMD":0.00,"IBDSYD":0.00,"IBDSLY":0.00,"IBDCMD":0.00,"IBDCYD":0.00,"IBDCLY":0.00,"IBNOMD":18.0,"IBNOYD":18.0,"IBNOLY":18.0,"IBPKMD":15.0,"IBPKYD":15.0,"IBPKLY":14.0,"IBINUS":" ","IBIAID":0.0,"IBSAID":0.0,"IBCQT1":1527.000,"IBCQT2":0.000,"IBCQT3":0.000,"IBFCST":"Y","IBDRSH":" ","IBWMIU":"JP","IBFL15":" ","IBUS20":" ","IBLPR1":0.00000,"IBLPR2":0.00000,"IBLPR3":0.00000,"IBLPR4":0.00000,"IBLPR5":0.00000,"IBLPCD":" ","IBABCC":"B","IBPRCL":0.0,"IBQBCL":" ","IBACDC":"Y","IBTDCD":" ","IBDOUM":" ","IBTP01":0.0,"IBTP02":0.0,"IBTP03":0.0,"IBTP04":0.0,"IBLMCC":20.0,"IBLMDT":130513.0,"IBTMPH":"Y","IBCOMC":" ","IBCOMF":0.00000,"IBITCT":" ","IBEOQT":0.000,"IBITCM":0.0,"IBBRVW":" ","IBPTID":" ","IBQTLT":0.0000,"IBCTY1":"AUS","IBCTY2":"AUS","IBTXCD":"1","IBREVS":"Y","IBITXC":" ","IBMNOQ":0.000,"IBSTUS":0.000,"IBUS30":" ","IBPSLN":" ","IBPLIN":"N","IBUPDP":"Y","IBDFII":"2011-08-11T00:00:00.000","IBLHRK":"A","IBPLNC":" "}]
My Controller:
[BreezeController]
public class ItemInquiryController : ApiController
{
readonly EFContextProvider<AplusEntities> _contextProvider = new EFContextProvider<AplusEntities>();
[System.Web.Http.HttpGet]
public string Metadata()
{
return _contextProvider.Metadata();
}
[HttpGet]
public IQueryable<ITBAL> ItemBalances(string itemNumber, string warehouse)
{
return _contextProvider.Context.ITBALs.Where(i => i.IBITNO == itemNumber && i.IBWHID == warehouse)
.OrderBy(i => i.IBWHID)
.ThenBy(i => i.IBITNO);
}
}
The relevant portion from the viewmodel:
var manager = new breeze.EntityManager("api/ItemInquiry");
var store = manager.metadataStore;
var itbalInitializer = function (itbal) {
itbal.CompositeKey = ko.computed(function () {
return itbal.IBITNO() + itbal.IBWHID();
});
};
store.registerEntityTypeCtor("ITBAL", null, itbalInitializer);
var index = "0" + (args.pageNum * args.pageSize);
var query = new breeze.EntityQuery("ItemBalances")
.withParameters({ itemNumber: "A100", warehouse: "1" })
.take(args.pageSize);
if (index > 0) {
query = query.skip(index);
}
manager.executeQuery(query).then(function (data) {
vm.itbals.removeAll();
var itbals = data.results;//[0].Data;
itbals.forEach(function (itbal) {
vm.itbals.push(itbal);
});
vm.totalRecords(1);
itemBalancesGrid.mergeData(vm.itbals(), args.pageNum, parseInt(vm.totalRecords()));
}).fail(function (e) {
logger.log(e, null, loggerSource, true, 'error');
});
I figure I must be missing something fairly simple, but it is escaping me.
UPDATE: I removed the BreezeController attribute from the ApiController, and it works correctly.
Jon, removing the [Breeze] attribute effectively disables breeze for your application so I don't think that is the long term answer to your problem.
If you don't actually want entities for this scenario - you just want data - than a Breeze projection that mentions just the data to display in the grid would seem to be the best choice. Projections return raw data that are not wrapped in KO observables and are not held in the Breeze EntityManager cache.
If you want the data as cached entities and you also want to display them in a grid that doesn't like KO observable properties ... read on.
You can unwrap a KO'd object with ko.toJS. However, the grid is likely to complain about circular references (or throw an "out of memory" exception as some grids do) ... even if the entity has no circular navigation paths. The difficulty stems from the fact that every Breeze entity has an inherent circularity by way of its entityAspect property:
something.entityAspect.entity //'entity' points back to 'something'
Because you are using Knockout for your model library and because you say ITBAL has no navigation properties ("is not related to anything"), I think the following will work for you:
manager.executeQuery(query).then(success) ...
function success(data) {
var unwrapped = ko.toJS(data.results).map(
function(entity) {
delete entity.entityAspect;
return entity;
});
vm.itbals(unwrapped);
vm.totalRecords(1); // huh? What is that "parseInt ..." stuff?
itemBalancesGrid.mergeData(vm.itbals(), args.pageNum, parseInt(vm.totalRecords()));
})
ko.toJS is a Knockout function that recursively unwraps an object or collection of objects, returning copies of values. Then we iterate over the copied object graphs, deleting their entityAspect properties. The array of results is stuffed into the vm.itbals observable and handed along.
You can imagine how to generalize this to remove anything that is giving you trouble.
Extra
What the heck is vm.totalRecords? I sense that this is supposed to be the total number of matching records before paging. You can get that from Breeze by adding .inlineCount() to the breeze query definition. You get the value after the query returns from the data.inlineCount property.
Do you really need vm.itbals()? If all you do here is pass values to the grid, why not do that and cut out the middle man?
The following success callback combines these thoughts
function success(data) {
var unwrapped = ko.toJS(data.results).map(
function(entity) {
delete entity.entityAspect;
return entity;
});
itemBalancesGrid.mergeData(unwrapped, args.pageNum, data.inlineCount);
})