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;
}
}
Related
I have created a list within this class called myVar...
class FailedIgnoredRetrieved
{
public static async Task FailedIgnoredRetrievedAndShow(ListView commentLabel)
{
void AllEventsComment(string comment)
{
if (commentLabel != null)
{
commentLabel.ItemsSource += comment; //+ "\n";
}
}
AllEventsComment("...");
var api = new ApiClient();
var retrieved = ApiToken.Load();
if (retrieved == null)
{
AllEventsComment("Failed, not signed in");
App.SwitchIcon(NotifyIcons.GreyIcon);
}
else
{
var result = await api.GetAllEventsAsync(retrieved.Token);
if (result.IsSuccessful)
{
List<string> myVar = new List<string>();
void AddToList(string v)
{
myVar.Add(v);
}
foreach (var eventsText in result.Events)
if (eventsText.EventStatus == 1)
{
AddToList($"Red {eventsText.CheckId}");
}
else if (eventsText.EventStatus == 0)
{
AddToList($"Orange {eventsText.CheckId}");
}
}
}
}
}
}
I now want to use myVar List in FailedIgnoredWindow.xaml.cs to bind to for a ListView in FailedIgnoredWindow.xaml I'm struggling to understand how to set it as the ListView.ItemSource = i.e. how do I access the list in the other class?
public partial class FailedIgnoredWindow : Window
{
public FailedIgnoredWindow()
{
InitializeComponent();
FailedIgnoredDialogue.ItemsSource =
}
private async void AllEvents_Click(object sender, RoutedEventArgs e)
{
AllEventsWindow win2 = new AllEventsWindow();
this.Visibility = Visibility.Hidden;
win2.WindowStartupLocation = 0;
//win2.Left = 0;
//win2.Top = 0;
win2.Show();
await AllEventsRetrieved.AllEventsRetrievedAndShowCount(win2.AllEventsDialogue);
}
}
I've tried to create a seperate class with the properties but I'm struggling to fully understand what I need to do.
myVar is declared inside the méthod so it will only be accessible inside this method.
If you want to access it from outside, you should make it a public property of the class FailedIgnoredRetrieved (note that in this case if you want to modify the property from the method FailedIgnoredRetrievedAndShow you will have to make it not static)
Another way would be to have the method return the list so you get it as result (the declaration would become Task<List<string>>FailedIgnoredRetrievedAndShow(... ).
There seems to be quite a bit of confusion in your code so I would recommend reading a tutorial about the scope of variable you can start here
You should also check this answer about the access modifiers here
I would also recommend staying away from local functions (functions inside functions) at the beginning as I think it adds to the confusion.
Let me know if it makes more sense after reading the links in my answer
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.
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.
This might be a silly question. But when I modified one example from Live SDK example, got a weird problem.
I was thinking the root cause is async function GetAll() was used synchronously.
Below is the code snippet, I put the problem as comments. Thanks in advance!
class SkyeDriveViewModel: INotifyPropertyChanged
{
private List<SkyDriveItem> folderList = null;
public List<SkyDriveItem> FolderList
{
get { return folderList; }
private set
{
if (value != folderList)
{
folderList = value;
NotifyPropertyChanged("FolderList");
}
}
}
private async void GetAll(string desiredPath)
{
FolderList = new List<SkyDriveItem>();
this.liveClient = new LiveConnectClient(SkyDrivePage.Session);
try
{
LiveOperationResult operationResult = await this.liveClient.GetAsync(desiredPath);
dynamic result = operationResult.Result;
dynamic items = result.data;
foreach (dynamic item in items)
{
SkyDriveItem newItem = new SkyDriveItem(item);
if (newItem.IsFolder)
{
FolderList.Add(newItem);
}
}
}
catch (LiveConnectException e)
{
}
//**till here, FolderList was assigned**
}
public void InitList()
{
Debugger.Log();
GetAll(SKYDRIVEINITPATH);
Debugger.LogWhen(eDebugger.LogTiming.Exit);
//**till here, FolderList had zero item**
}
}
In general having an async void function is a warning sign. You should only have such a method for an event handler. The appropriate return type for GetAll is Task, or possibly even Task<List<SkyDriveItem>>.
The issue is that calling GetAll will only execute code until it hits the first await call, at which point it returns to the caller and the remainder of the method will be executed asynchronously.
The problem here is that because the method is void you have no way of knowing when it's done. You have a "fire and forget" method and can never know when your list is actually ready. If GetAll returns a task then you can await that task and be sure that your list has actually been populated.
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);
}
}
}