I'm having some issues with UI updating within a BeginWrite transaction in Realm .NET / Xamarin.Forms
I have 2 models, ModelA and ModelB. ModelA contains an IList { get; } of ModelB.
I have 2 listview pages, the first populated with bound ModelA. Navigating through an entry takes you to the 2nd page with a listview bound to the ModelBs that ModelA contains and a button to add new ModelBs to ModelA.
If I do this within a Write transaction it will all work as intended. When you add a ModelB to ModelA you can see this in the ModelB list immediately and everything is persisted.
What I want is to do this within a BeginWrite and only commit when a save button is pressed. This works, but the ModelB list UI won't update. Only when navigating back into the page do the ModelB list entries appear.
Can this work the way I want?
Attached is my code from the 2nd page (listview of ModelA.ModelB) viewmodel (FreshMVVM being used).
public class SecondPageModel : FreshMvvm.FreshBasePageModel
{
Transaction _transaction;
public ModelA ModelA { get; set; }
public SecondPageModel() { }
public override void Init(object initData)
{
base.Init(initData);
ModelA = initData as ModelA;
var db = Realm.GetInstance();
_transaction = db.BeginWrite();
}
public Command NewModelB
{
get
{
return new Command(() =>
{
var newB = new ModelB();
newB.Name = "B";
ModelA.ModelBs.Add(newB);
});
}
}
public Command SaveModelA
{
get
{
return new Command(async () =>
{
_transaction.Commit();
await CoreMethods.PopPageModel();
});
}
}
protected override void ViewIsDisappearing(object sender, EventArgs e)
{
base.ViewIsDisappearing(sender, e);
_transaction.Dispose();
}
}
The collection change notifications are emitted from the database itself. So until something is saved to the database, nothing has changed (thus you only get the notifications after the commit). One way to work around it is to instead create the object and save it eagerly, but mark it as in-progress. Then on save, remove the flag and on disappearing, delete all incomplete objects:
public Command NewModelB
{
get
{
return new Command(() =>
{
db.Write(() =>
{
ModelA.ModelBs.Add(new ModelB
{
Name = "B",
InProgress = true
});
});
});
}
}
public Command SaveModelA
{
get
{
return new Command(async () =>
{
db.Write(() =>
{
var toCommit = ModelA.ModelBs.Where(m => m.InProgress);
foreach (var modelB in toCommit)
{
modelB.InProgress = false;
}
});
await CoreMethods.PopPageModel();
});
}
}
protected override void ViewIsDisappearing(object sender, EventArgs e)
{
base.ViewIsDisappearing(sender, e);
db.Write(() =>
{
var toDelete = ModelA.ModelBs.Where(m => m.InProgress).ToArray();
foreach (var modelB in toDelete)
{
db.Remove(modelB);
}
});
}
Obviously, this may leave some dangling objects if the app crashes or if the user quits it before navigating away, so you'll probably want to add some cleanup logic in your application startup.
Related
I am stuck on problem where my facade method is never called so my returned items are always zero. I dont exactly know why this happens since it should at least load items on initial page load. Also I got almost identical ViewModel where it works fine.
Here is my viewmodel where I want to load FavouriteCommodities into GridViewDataSet
//initialized via constructor
private readonly FavouriteCommodityFacade _favouriteCommodityFacade;
public GridViewDataSet<FavouriteCommodityDTO> GridViewDataSetItems { get; set; }
public int PageSize { get; set; } = 10;
private ISortingOptions DefaultSortOptions => new SortingOptions();
public override Task Init()
{
GridViewDataSetItems = new GridViewDataSet<FavouriteCommodityDTO>()
{
PagingOptions = { PageSize = PageSize },
SortingOptions = DefaultSortOptions
};
return base.Init();
}
public override Task PreRender()
{
if (!Context.IsPostBack || GridViewDataSetItems.IsRefreshRequired)
{
LoadData();
}
return base.PreRender();
}
public void LoadData()
{
FavouriteCommodityGroups = _userAccountFavouriteProductsFacade.GetAllFavouriteProductsToUser();
//this never fires my facade method below
GridViewDataSetItems.OnLoadingData = option => _favouriteCommodityFacade.GetAssignedFavouriteProductsToGroup(option);
}
Here is my method in my facade which is never fired.
public GridViewDataSetLoadedData<FavouriteCommodityDTO>
GetAssignedFavouriteProductsToGroup (IGridViewDataSetLoadOptions gridViewDataSetLoadOption)
{
using (unitOfWorkProvider.Create())
{
var query = _favouriteCommodityByChosenGroupQuery();
FavouriteProductByGroupFilter.FavouriteGroupId = 16;
query.Filter = FavouriteProductByGroupFilter;
var x = GetGridViewDataSetLoadedData(gridViewDataSetLoadOption, query);
return x.Result;
}
}
I am also wondering if there is possibility to load those items on user click event. When I looked into namespace DotVVM.Framework.Controls I didnt find anything useful for that.
So I figure out after a while that Repeater component not triggering method. I had to use only GridView component in view.
I have a collection of classes contained in a ObservibaleCollection<MyObj> and MyObj implements INotifyPropertyChanged, but I need a property located outside of it that references a property in the collection via linq and creates its own collection to update on both the collection change and any of its content linq bound properties changing.
For sake of argument and simplicity lets say my class MyObj contains a property called IsVisible. I want a property that implements its own INotifyPropertyChanged to get a list of MyObj where IsVisible == true and keep it up to date regardless id the collection of MyObj changes or the IsVisible property does.
Is this possible without attaching to the collection changed event and subsequently just directly attaching to each child MyObj.IsVisible property? Is there a way to get INotify to bubble up through linq?
public class MyObj:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public bool IsVisible
{
get { return _IsVisible; }
protected set { if (value != _IsVisible) { _IsVisible= value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("IsVisible")); } }
}
private bool _IsVisible;
}
public class Foo
{
ObservableCollection<MyObj> myObjs = new ObservableCollection<MyObj>();
ObservableCollection<MyObj> myVisibleObjs {
get{
return myObjs.where(o => o.IsVisible);
}
}
}
I hope what I'm asking makes sense.
You could make use of Reactive Extensions, but for this specific requirement - maintaining a myVisibleObjs - I would use the dynamic data lib.
Try out the following:
static void Main(string[] args)
{
Foo f = new Foo();
f.BindToVisibleObjects();
// add more dummy data
f.Add(false);
f.Add(true);
// There will be 2 visible objects in MyVisibleObjects
foreach (var d in f.MyVisibleObjects)
{
Console.WriteLine(d.IsVisible);
}
Console.ReadKey();
}
public class Foo
{
ObservableCollection<MyObj> myObjs = new ObservableCollection<MyObj>();
public ReadOnlyObservableCollection<MyObj> MyVisibleObjects;
public Foo()
{
// add some dummy data
myObjs.Add(new MyObj() { IsVisible = true });
myObjs.Add(new MyObj() { IsVisible = false });
}
public void BindToVisibleObjects()
{
myObjs.ToObservableChangeSet()
.Filter(o => o.IsVisible)
.Bind(out MyVisibleObjects)
.DisposeMany()
.Subscribe();
}
public void Add(bool vis)
{
myObjs.Add(new MyObj() { IsVisible = vis });
}
}
The key here is that we bind the filtered observable changeset to a new collection that will be updated as your myObjs changes.
Following is my ViewModel class:
public partial class DosAdminProductHierarchy : UserControl, INotifyPropertyChanged
{
public DosAdminProductHierarchy()
{
InitializeComponent();
this.GetProductList();
//this.ProductList = new NotifyTaskCompletion<List<Product>>(this.GetProductList());
OnPropertyChanged("DepartmentList");
if(isDataLoaded)
{
treeList.ItemsSource = ProductList;
treeList.Visibility = Visibility.Visible;
}
}
private ObservableCollection<Product> dbProductList;
private bool isDataLoaded = false;
public ObservableCollection<Product> ProductList
{
get
{
return dbProductList;
}
private set
{
dbProductList = value;
isDataLoaded = true;
}
}
private async void GetProductList()
{
try
{
IWebApiDataAdapter _webAPIDataAdapter = new DosAdminDataAdapter();
List<Product> lstProd= new List<Product>();
lstProd = await _webAPIDataAdapter.GetProductHierarchy();
dbProductList = new ObservableCollection<Product>();
foreach (Product prd in lstProd)
{
dbProductList.Add(prd);
}
}
catch (Exception ex)
{
throw (ex);
}
}
}
My problem is I want ProductList to be populated but it is not getting populated. Execution is reaching till the end of constructor even though data has not returned from WebApi, I want somehow to hold the execution or to show user something is busy till ProductList is getting populated. Please help how to achieve that.
You should not be loading data in a constructor. It violates the S principle of SOLID.
You should be using a Command linked to Loaded event or similar to load data.
You should also not be using async void method signatures as it hides Exception thrown by the method.
Your constructor is returning immediately because you are not calling await GetProductsList(). Your code results in the async method being executed after the constructor completes.
To solve your problem with the visibility, rather use a BooleanToVisibilityConverter on a Binding to IsDataLoaded property and make it notify when it changes value.
I'm using IMobileServiceTable in an data access layer class and bind it to an listview. Initial loading works fine but filtering doesn't. It always returns the initial loaded data.
public class ItemsManager {
IMobileServiceTable<Item> itemTable;
public ItemsManager (IMobileServiceTable<Item> todoTable)
{
this.itemTable = todoTable;
}
public async Task<List<Item>> GetTasksAsync (string searchString)
{
//following doesn't work
var list = new List<Item> (await itemTable.Where(x => x.ItemID.Contains(searchString)).ToListAsync());
return list;
}
public async Task<List<Item>> GetTasksAsync ()
{
return new List<Item> (await itemTable.OrderBy(a =>a.ItemID).ToListAsync());
}
}
If it matter, following is my page code :
public partial class ItemsListXaml : ContentPage
{
IMobileServiceTable<Item> itemTable;
ItemsManager itemManager;
public ItemsListXaml ()
{
InitializeComponent ();
itemTable = App.client.GetTable<Item>();
itemManager = new ItemsManager(itemTable);
App.SetItemsManager (itemManager);
}
protected async override void OnAppearing ()
{
base.OnAppearing ();
listView.ItemsSource = await itemManager.GetTasksAsync ();
}
async void OnValueChanged (object sender, TextChangedEventArgs e) {
var t = e.NewTextValue;
// perform search on min 3 keypress
if (t.Length>3) {
listView.ItemsSource = await itemManager.GetTasksAsync(SearchFor.Text);
}
}
}
It looks like the problem line is this:
var list = new List (await
itemTable.Where(x => x.ItemID.Contains searchString)).ToListAsync());
Not sure exactly what's going on there, but I did manage to get something similar working. The sample I have uses a proxy object to save on Azure fetches. I fetch once to get the initial list of tasks and save that to a local ObservableCollection object that I can bind to the list. Then, I can filter the collection object that is bound to the list (sample here).
You might have legitimate reasons for fetching a filtered list from Azure. In my mind - and bear with me because I'm no expert on app design - unless there is a significant period of time between the initial fetch of the list and the filter action where there might be new data introduced to the table, seems like just filtering a local object would perform better and be cheaper. The app could always handle push notifications to update the list as needed.
Basically, pull objects from Azure into it as shown here:
public async Task<ObservableCollection<ToDoItem>> GetTasksAsync()
{
try
{
return new ObservableCollection<ToDoItem>(await _todoTable.ReadAsync());
}
catch (MobileServiceInvalidOperationException msioe)
{
Debug.WriteLine(#"INVALID {0}", msioe.Message);
}
catch (Exception e)
{
Debug.WriteLine(#"ERROR {0}", e.Message);
}
return null;
}
Then, bind to list as shown here:
protected async override void OnAppearing()
{
base.OnAppearing();
App.TodoManager.TodoViewModel.TodoItems = await App.TodoManager.GetTasksAsync();
listViewTasks.ItemsSource = App.TodoManager.TodoViewModel.TodoItems;
}
In this example, “App.TodoManager.TodoViewModel.TodoItems” is the fully qualified path to the proxy object which is the ObservableCollection.
Then you can filter the proxy object and rebind it to the list. I haven’t actually implemented that part in the sample, but I did take down a copy of it and then added the code and seems to work fine. This would be the code:
Getting the filtered list:
public ObservableCollection<ToDoItem> GetFilteredList(string searchString)
{
return new ObservableCollection<ToDoItem>
(TodoViewModel.TodoItems.Where(x => x.Name.Contains(searchString)));
}
Calling helper method and binding to listview (Incorporating this into one of your example blocks):
async void OnValueChanged (object sender, TextChangedEventArgs e) {
var t = e.NewTextValue;
// perform search on min 3 keypress
if (t.Length>3) {
App.TodoManager.TodoViewModel.TodoItems = App.TodoManager.GetFilteredList(searchFor.Text);
listViewTasks.ItemsSource = App.TodoManager.TodoViewModel.TodoItems;
}
}
I'm updating an ObservableCollection of a WPF ViewModel in a WCF Data Service asynchronous query callback method:
ObservableCollection<Ent2> mymodcoll = new ObservableCollection<Ent2>();
...
query.BeginExecute(OnMyQueryComplete, query);
...
private void OnMyQueryComplete(IAsyncResult result)
{
...
var repcoll = query.EndExecute(result);
if (mymodcoll.Any())
{
foreach (Ent c in repcoll)
{
var myItem = mymodcoll.Where(p => p.EntID == c.EntID).FirstOrDefault();
if (myItem != null)
{
myItem.DateAndTime = c.DateAndTime; // here no problems
myItem.Description = c.Description;
...
}
else
{
mymodcoll.Add(new Ent2 //here I get a runtime error
{
EntID = c.EntID,
Description = c.Description,
DateAndTime = c.DateAndTime,
...
});
}
}
}
else
{
foreach (Ent c in repcoll)
{
mymodcoll.Add(new Ent2 //here, on initial filling, there's no error
{
EntID = c.EntID,
Description = c.Description,
DateAndTime = c.DateAndTime,
...
});
}
}
}
The problem is, when a query result collection contains an item which is not present in the target collection and I need to add this item, I get a runtime error: The calling thread cannot access this object because a different thread owns it. (I pointed out this line of code by a comment)
Nevertheless, if the target collection is empty (on initial filling) all items have been added without any problem. (This part of code I also pointed out by a comment). When an item just needs to update some of its fields, there are no problems as well, the item gets updated ok.
How could I fix this issue?
First case: Here you a modifying an object in the collection, not the collection itself - thus the CollectionChanged event isn't fired.
Second case: here you are adding a new element into the collection from a different thread, the CollectionChanged event is fired. This event needs to be executed in the UI thread due to data binding.
I encountered that problem several times already, and the solution isn't pretty (if somebody has a better solution, please tell me!). You'll have to derive from ObservableCollection<T> and pass it a delegate to the BeginInvoke or Invoke method on the GUI thread's dispatcher.
Example:
public class SmartObservableCollection<T> : ObservableCollection<T>
{
[DebuggerStepThrough]
public SmartObservableCollection(Action<Action> dispatchingAction = null)
: base()
{
iSuspendCollectionChangeNotification = false;
if (dispatchingAction != null)
iDispatchingAction = dispatchingAction;
else
iDispatchingAction = a => a();
}
private bool iSuspendCollectionChangeNotification;
private Action<Action> iDispatchingAction;
[DebuggerStepThrough]
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!iSuspendCollectionChangeNotification)
{
using (IDisposable disposeable = this.BlockReentrancy())
{
iDispatchingAction(() =>
{
base.OnCollectionChanged(e);
});
}
}
}
[DebuggerStepThrough]
public void SuspendCollectionChangeNotification()
{
iSuspendCollectionChangeNotification = true;
}
[DebuggerStepThrough]
public void ResumeCollectionChangeNotification()
{
iSuspendCollectionChangeNotification = false;
}
[DebuggerStepThrough]
public void AddRange(IEnumerable<T> items)
{
this.SuspendCollectionChangeNotification();
try
{
foreach (var i in items)
{
base.InsertItem(base.Count, i);
}
}
finally
{
this.ResumeCollectionChangeNotification();
var arg = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
this.OnCollectionChanged(arg);
}
}
}