my development team and I have run into an issue on our Android distribution of our Xamarin project. The issue is as such: The application uses an observable collection of objects and represents these objects in the form of a list view and a map view with pins representing the objects. In the map view, our code is designed to subscribe to a messaging center call that periodically updates the observable collection of objects from our API (other part of project). The issue we are having is that when we call PlotPins method in the messaging center code block, the application should first retrieve the updated list and then access that list to plot pins on the map. Every time an update is received, the application will clear all pins from the map and then replot the pins based on the updated list (inefficient we know, but this is a temporary solution). However, the pins are never updated. Through the use of the debugger we have discovered that once map.Pins.Clear() within PlotPins() is called, the application jumps to the end of the RequestUpdatedListAsync method (which occurs periodically to retrieve the updated list and which triggers the Messaging Center) and then halts.
Our solution works for our GTK build, with the pins being cleared and redrawn on the map as intended, so this seems to be an Android specific issue.
Any help would be appreciated, thank you.
Relevant code located below:
MESSAGING CENTER:
MessagingCenter.Subscribe<object, ObservableCollection<MyObject>>(Application.Current, Constants.ListUpdateContract, (sender, newList) =>
{
//update list
listUpdater.UpdateList(newList);
//call method to plot pins again
PlotPins(map);
});
PLOTPINS:
private void PlotPins(Map map)
{
map.Pins.Clear();
foreach (MyObject in MyObjects)
{
var pin = new Pin
{
Label = MyObject.ID,
Address = "Latitude: " + MyObject.Latitude + " " + "Longitude: " + MyObject.Longitude,
Type = PinType.Place,
Position = new Position(Convert.ToDouble(MyObject.Latitude), Convert.ToDouble(MyObject.Longitude))
};
//event handler for when user clicks on pin's info window
pin.InfoWindowClicked += async (s, args) =>
{
//opens up detail page for pin associated with myObject
await Navigation.PushAsync(new DetailPage(MyObject));
};
map.Pins.Add(pin);
}
}
REQUEST UPDATED LIST ASYNC:
public static async System.Threading.Tasks.Task<bool> RequestUpdatedListAsync()
{
if (!_tokenIsGood)
return false;
var success = false;
var response = new HttpResponseMessage();
try
{
response = await _client.GetAsync(Constants. MyObjectDisplayUrl);
}
catch (Exception e)
{
System.Diagnostics.Debug.WriteLine("Error requesting updated list.");
System.Diagnostics.Debug.WriteLine(e.Message);
System.Diagnostics.Debug.WriteLine(e.StackTrace);
return success;
}
response.EnsureSuccessStatusCode();
success = true;
var responseBody = await response.Content.ReadAsStringAsync();
// Update list
MyObjects.Clear();
MyObjects = JsonConvert.DeserializeObject<ObservableCollection< MyObject >>(responseBody);
//Alert subscribed ViewModels to update list
MessagingCenter.Send<object, ObservableCollection< MyObject >>(Application.Current, Constants.ListUpdateContract, units);
return success;
}
Since maps.Pins is UI related it has to be run in main UI thread.
MessagingCenter doesnt always publish/subscribe in main threads .
So to fix this issue call the maps.Pins.Clear() in main thread.
Device.BeginInvokeOnMainThread(()=> maps.Pins.Clear());
Credits: #shanranm for mentioning limitation of MessagingCenter for using main threads.
Related
I am using Realm MongoDB for my android app, and I have a problem:
I have different users in my app, and each user has his "cards". The partition of each user's cards is:
"Card=userID".
So, I want to be able to send a card from one user to the other. I do it via a link that includes userID and specific cardID.
So my code looks something like:
Realm.init(this);
mainApp = new App(new AppConfiguration.Builder(APP_ID).defaultSyncErrorHandler((session, error) ->
Log.e("TAG()", "Sync error: ${error.errorMessage}")
).build());
//TEMP CODE
String partition = "Card=611d7n582w36796ce34af106"; //test partition of another user
if(mainApp.currentUser() != null) {
SyncConfiguration config = new SyncConfiguration.Builder(
mainApp.currentUser(),
partition)
.build();
Realm realmLinkCard = Realm.getInstance(config);
Log.d(TAG, "onCreate: cards found- " + realmLinkCard.where(Card.class).findAll().size());
}
The last log always shows 0. I know there are cards for sure because if the user that created the corresponding partition is signed in then it does find the cards.
permissions are set to true for both read and write for the whole sync.
What can the problem be?
You cannot access a Realm by a user who has a different partition.
Instead you can create a mongodb function and call it from your user.
Make your function here:
Check here on How to create a function
And call it by checking here on How to call a function from client
Quick example of a realm function:
exports = async function funcName(partition) {
const cluster = context.services.get('myclustername');
const mycollection = cluster.db('mydbname').collection('mycollectionname');
let result = [];
try {
result = mycollection.findOne({
_partition: partition,
});
} catch (e) {
result.push(e);
return result;
}
return result;
};
To call it, please see above for the documentation as I'm not an Android developper.
If I try to send data to FirebaseDatabase in the absence of an Internet connection, then, as expected, nothing happens. If you turn on the Internet, then this data is added themselves, and even if I restarted the application. And they are added to the very end of the database. Even if you add data after this, the offline post will still be the last.
I think to solve the problem by checking the Internet before sending. Maybe there are any other solution methods?
I send data in a simple way:
final String phone_val = etPhone.getText().toString().trim();
final String comment_val = etComment.getText().toString().trim();
DatabaseReference newTrip = mDatabase.push();
newTrip.child("phone").setValue(phone_val);
newTrip.child("comment").setValue(comment_val);
newTrip.child("type").setValue(1);
startActivity(new Intent(AddDriverActivity.this, TripActivity.class));
finishAffinity();
Firstly, if you haven't already, read through the offline capabilities documentation so you have a general grasp of how Firebase behaves while offline.
Next, we'll clean up your write operations so that they are a single atomic operation rather than a few separate write operations.
HashMap<String, Object> tripData = new HashMap<>();
tripData.put("phone", phone_val);
tripData.put("comment", comment_val);
tripData.put("type", 1);
DatabaseReference newTrip = mDatabase.push();
newTrip.setValue(tripData);
As stated in the offline capabilities documentation, you can check whether your app is offline by checking the special database location /.info/connected which returns the current connection state. This value will be either true or false.
While you could check this value before posting your trips, the connection state may change while you are sending the data.
Even if you add data after this, the offline post will still be the last.
This is the trickier part to manage. I think the easiest way to deal with this is to have a "staging" section of your database and then move data as it is created at this location to the main storage location using a Cloud Function for Firebase.
Client Side
Let's say you are storing these trips in /trips/someTripId.
private DatabaseReference mDatabase = FirebaseDatabase.getInstance().getReference();
private DatabaseReference mAllTripsRef = mDatabase.child('trips');
To add a new trip, you would use:
HashMap<String, Object> tripData = new HashMap<>();
tripData.put("phone", phone_val);
tripData.put("comment", comment_val);
tripData.put("type", 1);
DatabaseReference mNewTripRef = mAllTripsRef.push();
mNewTripRef.setValue(tripData);
Because references created by push() are based on the estimated time of the Firebase servers, they will be ordered by when they were created rather than when they are received by the Firebase servers. But if you wanted to preserve that offline trips are always last, instead of writing new trips to /trips, you would instead write to /trips-staging.
private DatabaseReference mDatabase = FirebaseDatabase.getInstance().getReference();
// trips that have been posted to "online" database
private DatabaseReference mAllTripsRef = mDatabase.child('trips');
// trips yet to be posted online
private DatabaseReference mStagingTripsRef = mDatabase.child('trips-staging');
New data would be added using:
HashMap<String, Object> tripData = new HashMap<>();
tripData.put("phone", phone_val);
tripData.put("comment", comment_val);
tripData.put("type", 1);
DatabaseReference mNewTripRef = stagingTripsRef.push();
mNewTripRef.setValue(tripData);
Now that we have the reference to the trip waiting to be posted, mNewTripRef, we can add a listener to it to see when it has been posted.
In the cloud side below, we are going to make it so that if there is data at /trips-staging/someTripId and it is just a string, then the trip has been received and posted by the server to the location /trips/<string-value>.
ValueEventListener stagingTripListener = new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
// Get trip data
Object tripData = dataSnapshot.getValue();
if (tripData == null) {
// Data has been deleted!
// Disconnect this listener
mNewTripRef.removeEventListener(this);
// TODO: What now?
} else if (tripData instanceof String) {
// Data has been moved!
DatabaseReference postedTripRef = mAllTripsRef.child((String) tripData);
// Disconnect this listener
mNewTripRef.removeEventListener(this);
Log.i(TAG, "stagingTripListener:onDataChange", "New trip has been successfully posted as trip '" + mNewTripRef.getKey() + "'");
// TODO: do something with postedTripRef
} else {
// ignore - the trip hasn't been moved yet, continue waiting
// tripData is a Map<string, Object> with our original data
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
// Getting this trip failed, log a message
Log.w(TAG, "stagingTripListener:onCancelled", databaseError.toException());
}
};
mNewTripRef.addValueEventListener(stagingTripListener);
Cloud Side
Now, we need to move these new trips over to /trips once they are received on the server. For this we can use a Cloud Function for Firebase that listens to Realtime Database events. For our use case, we want to exclusively listen to data creation events that happen on our /trips-staging location.
When the Cloud Function is triggered, it should take the data at /trips-staging/someId and move it to /trips/someNewId. It is probably also a good idea to store where we moved the data to at the old location if it is ever needed but also so we can tell when the trip has been received by the server.
After following the Getting Started documentation up to Step 4, you can use the following code as your index.js or index.ts file and then deploy it using firebase deploy --only functions.
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp(); // use defaults
export const moveTripsFromStaging = functions.database.ref('/trips-staging/{stagingTripId}')
.onCreate((snapshot, context) => {
const stagingTripId = snapshot.key;
const tripData = snapshot.val();
// get reference to the root of the database
let dbRootRef = admin.database().ref();
let allTripsRef = dbRootRef.child('trips');
let newTripRef = allTripsRef.push();
return dbRootRef.update({
// move data to it's new home
['trips/' + newTripRef.key]: tripData,
// save where we moved data to as a simple string containing the new key in /trips
['trips-staging/' + stagingTripId]: newTripRef.key // or set to null to delete it
});
})
Once deployed, you should see new trips that are uploaded to /trips-staging be received by the server and then moved across to /trips in the order that server receives them.
I have an odd issue I can't explain the reason for it - maybe someone here can shed some light on it
I have a ticket scanning app in Xamarin Forms currently testing it on android
the interface allows you to:
type an order number and click the check order Button
use the camera scanner to scan which automatically triggers check order
use the barcode scanner to scan which automatically triggers check order
after the check order validation, user has to select the number of tickets from a drop down list and press confrim entry button
what I'm trying to do, is if the seats available on that ticket is just 1 - then automatically trigger confirm entry button functionality
problem that I have is that - some of my logic depends on setting the drop down index in code - for some reason it doesn't update - as seen in the debugger shot here
and this is the second tme I've noticed this today, earlier it was a var I was trying to assign a string and it kept coming up as null - eventually I replaced that code
is this a bug in xamarin ?
code has been simplified:
async void OnCheckOrderButtonClicked(object sender, EventArgs e)
{
await ValidateOrderEntry();
}
private async void scanCameraButton_Clicked(object sender, EventArgs e)
{
messageLabel.Text = string.Empty;
var options = new ZXing.Mobile.MobileBarcodeScanningOptions();
options.PossibleFormats = new List<ZXing.BarcodeFormat>() {
ZXing.BarcodeFormat.QR_CODE,ZXing.BarcodeFormat.EAN_8, ZXing.BarcodeFormat.EAN_13
};
var scanPage = new ZXingScannerPage(options);
scanPage.OnScanResult += (result) =>
{
//stop scan
scanPage.IsScanning = false;
Device.BeginInvokeOnMainThread(async () =>
{
//pop the page and get the result
await Navigation.PopAsync();
orderNoEntry.Text = result.Text;
//automatically trigger update
await ValidateOrderEntry();
});
};
await Navigation.PushAsync(scanPage);
}
private async Task ValidateOrderEntry()
{
//...other code....
checkInPicker.Items.Clear();
if (availablTickets == 1)
{
checkInPickerStack.IsVisible = true;
checkInPicker.SelectedIndex = 0;
messageLabel.Text = "Ticket OK! - " + orderNoEntry.Text;
messageLabel.TextColor = Color.Green;
//select the only element
checkInPicker.SelectedIndex = 0;
await PostDoorEntry();
}
//...other code....
}
private async Task PostDoorEntry()
{
int entryCount = checkInPicker.SelectedIndex + 1;
//... more code...
//...post api code..
}
Maybe I'm overlooking something, but you clear all the items a few lines above the one you are pointing out. That means there are no items in your Picker and thus you can't set the SelectedIndex to anything other than -1, simply because there are no items.
I am building a Cordova app for Android. I have to parse a JSON that consists of posts. Each post has text (title, description, category etc.) and images (an array of images - can be one or more). My aim is to store the JSON data for offline use (save to SQLlite database). Right now the example code below works, but the sequence is not how I expected to be:
request JSON (ok)
Wait for all promises (ok)
Parse JSON (finishes before all the images are downloaded)
Store to database the information but the images still downloading (in background thread - no harm for the UI).
What I would like to have is to store to database, when all the images have been downloaded. I' ve tried many things such as replacing the second for-loop with a recursive function (to handle the async function as stated here) and many other similar approaches but I believe that the problem starts from the 1st for loop which doesn't wait for the checkCache to finish. What do you think? How can I overcome this issue? If you need any further explanation just ask me.
My setup is:
Cordova 4.0.0, Angular 1.3.1 and ImgCache 1.0
My approach is:
1st. Request JSON:
promise1 = $http({method: 'GET', url: baseURL + options1};
promise2 = $http({method: 'GET', url: baseURL + options2};
//...
2nd. Wait for all promises
return $q.all([promise1,promise2,...]).then(function(data){
var promise1size = data[0].data.posts_number;//posts.length;
parseJSON(data[0],promise1size,'category1');
var promise2size = data[1].data.posts_number;//posts.length;
parseJSON(data[1],promise1size,'category2');
//similar for the rest promises
});
3rd. Parse JSON
function parseJSON(respdata,size,category){
console.log("Parsing "+size+" "+category);
for(i=0;i<size;i++){
var item = {};
item ["id"] = respdata.data.object[i].id;
item ["title"] = respdata.data.object[i].title;
item ["description"] = respdata.data.object[i].description;
var jsarray = respdata.data.object[i].categories;
item ["category"] = jsarray[0].title;
item ["catid"] = jsarray[0].id;
//Other JSON keys here similar as above
//Here it starts...
var jsattachement = respdata.data.object[i].attachments;
var atsize = jsattachement.length;
if(atsize>0){
var images=[];
for(j=0;j<atsize;j++){
(function(j){checkCache(jsattachement[j].url)}(j));//here is the problem
multimedia.push({title:item["title"], src:ImgCache.returnCachedURL(jsattachement[j].url), w:400,h:300});
images.push({title:item["title"],src:ImgCache.returnCachedURL(jsattachement[j].url),w:400,h:300});
}
item ["attachement"] = images;
}else
item ["attachement"] = [];
if(category=='category1')
response.category1.push(item);
else if(category=='category2')
response.category2.push(item);
//else if...
//else if...
}
}
};
checkCache function:
function checkCache (imgsrc){
ImgCache.isCached(imgsrc, function(src, success) {
if(!success){
ImgCache.cacheFile(src, function(){});
}
});
};
4th. Store to database
Here I save the parsed information to the database. On step 3 I use the returnCachedURL function for the images (which is not asynchronous) so to have the local path of the image ready even if it might not have been downloaded yet (but eventually will).
I did this:
Similar to you but, use update sql to store the image for every downloaded image. Then, I found that some of my users want to play with me, they disconnect the internet connection in the middle of image downloading! so that I have incomplete record in the sql! Jesus!
then I change to this: I create a temporary global var. e.g. tempvar = {}, then: tempvar.data1 = 'hello', tempvar.preparedatacompleted = true, tempvar.ImagesToBeDownload = 5, tempvar.ImagesDownloaded = 0, tempvar.imagearray = ......
then, everytime image downloaded, add 1 to the number, if that no. = total no., call the function that save all data and images in this tempvar to the sql.
I have used Google Api in order to retrive places details but Google Api only returns 20 results at a time according to category. JSON parsed text contains next_page_token which have referance that could be used to retrive another page containing results and so on. My question is how to put parsing in loop so that i can get all the total results.
So you have to build up a list of all results, but not invoke your callback until all results are downloaded. In the code below, we keep calling the function to get the next page of results until the next_page_token isn't available, in which case we've loaded all results and can invoke our callback function.
var jsonresults = [];
function getPlacesResults(url, callback) {
var xhr = Ti.Network.createHTTPClient({...});
xhr.onload = function (data) {
var json = JSON.parse(data);
jsonresults.push(json)
if (json.next_page_token) {
//assumes next_page_token is a url we can pass in
getPlacesResults(json.next_page_token, callback);
} else {
callback(jsonresults);
}
}
xhr.open('GET', url);
xhr.send();
}
//used like so
getPlacesResults('http://www.google.com/whatever', function (allResults) {
//...do stuff with all of your results here.
});