Long time lurker (especially on this problem). I have been studying asynchronous posts on this website directly related to my problem, but I have not found a solution. I have also tried many different variations of the below code to no avail.
What is happening is I asynchronously execute ps.BeginInvoke() after loading commands into it, use some sort of method to launch the invocation asynchronously, and it has indeed always run asynchronously! The problem though is that (for example), the commands executed will only return results sometimes 2 out of 10 times, sometimes 8 out of 10 times. Here is an example of the current code I am executing:
internal class aSyncPowershellCommand
{
//private static Collection<Collection<PSObject>> GlobalResults = new Collection<Collection<PSObject>>();
// This exists simply
public bool Finished { get; set; }
private object instanceLock = new object();
private PowerShell ps;
// private Guid uniqueGuid = Guid.NewGuid();
private PSDataCollection<PSObject> psresult;
public async Task<bool> PowershellLaunchPool(Dictionary<ARS_GroupObject, string> itemListing)
// itemListing contains a custom class that specified details about an ARS group object
// and the String "Value" contains the action (in this case, "get-QADGroup".
{
// Create a common RunspacePool --- it should be noted that for testing purposes, I am sending exactly 8 groups as
// part of the query, for testing purposes.
RunspacePool pool = RunspaceFactory.CreateRunspacePool(1, 8);
pool.CleanupInterval = new TimeSpan(0, 5, 0);
pool.Open();
// Create a spot to track the results in a unique way (I have experimented with lists, dictionaries, etc.... the results are
// always the same though, this just happens to be my latest method to attempt to avoid the "Key already added" error"
ConcurrentDictionary<Guid, IAsyncResult> powershellCommandResults = new ConcurrentDictionary<Guid, IAsyncResult>();
ConcurrentDictionary<Guid, PowerShell> powershellSessions = new ConcurrentDictionary<Guid, PowerShell>();
// Locking the instance, again, not sure if this actually does anything as far as my code is concerned, but it's
// still keeping in line with the same results as before.... semi-success and a lot of "key already added" errors.
lock (instanceLock)
{
foreach (var grpPair in itemListing)
{
// Create a unique GUID for the dictionary, just in case this is what was triggering the "key already added" error
Guid uniqueGuid = Guid.NewGuid();
// This passes text output to a textbox, not important
StatusUpdater.PassPowershellStatus(uniqueGuid.ToString() + " - 1\n");
// Create a fresh PowerShell instance
this.ps = PowerShell.Create();
//StatusUpdater.PassPowershellStatus(this.ps.InstanceId.ToString());
// Add this fresh PowerShell instance to the common RunspacePool
this.ps.RunspacePool = pool;
// Create a fresh instance of psresult to keep track of changes. This is honestly one piece of code I am suspicious
// of as far as throwing the "key already added" error, but I haven't been able to isolate.
this.psresult = new PSDataCollection<PSObject>();
// grpPair.value is "get-QADGroup"
this.ps.Commands.AddCommand(grpPair.Value);
//grpPair.Key.Groupname ---- name of the group
this.ps.Commands.AddParameter("identity", grpPair.Key.GroupName);
// loading up the collection of PowerShell sessions to keep track of, trying as hard as possible to keep this
// all as unique as I possibly can.
powershellSessions.TryAdd(uniqueGuid, this.ps);
// Reference event handlers for when something happens with the variable psResult
this.psresult.DataAdded += new EventHandler<DataAddedEventArgs>(tmpInstance_DataAdded);
this.ps.InvocationStateChanged += new EventHandler<PSInvocationStateChangedEventArgs>(ps_InvocationStateChanged);
}
}
foreach (var execute in powershellSessions)
{
// Looping through all of the PowerSHell sessions queue up and invoking, relying on their same instance of psresult
// to return the results and trigger the tmpInstance_DataAdded and ps_InvocationStateChanged events.
powershellCommandResults.TryAdd(execute.Key, execute.Value.BeginInvoke<PSObject, PSObject>(null, this.psresult));
}
while (Finished != true)
{
Thread.Sleep(3000);
}
return true;
}
private void tmpInstance_DataAdded(object sender, DataAddedEventArgs e)
{
PSDataCollection<PSObject> tmpReturn = (PSDataCollection<PSObject>)sender;
foreach (var item in tmpReturn)
{
StatusUpdater.PassPowershellStatus(item.BaseObject.ToString() + "\n");
}
}
private void ps_InvocationStateChanged(object sender, PSInvocationStateChangedEventArgs e)
{
var psInfo = (PowerShell)sender;
if (e.InvocationStateInfo.State == PSInvocationState.Completed)
{
StatusUpdater.PassPowershellStatus("RESULT --- " + e.InvocationStateInfo.Reason.ToString() + "\n");
psInfo.Stop();
psInfo.Dispose();
psInfo = null;
}
if (e.InvocationStateInfo.State == PSInvocationState.Failed)
{
StatusUpdater.PassPowershellStatus("*** ERROR - \n" + e.InvocationStateInfo.Reason.ToString() + " ***\n");
psInfo.Stop();
psInfo.Dispose();
psInfo = null;
// StatusUpdater.PassPowershellStatus(e.InvocationStateInfo.Reason.ToString());
}
}
}
I have tried different combinations of IASyncResult result = ps.BeginInvoke() and awaiting that command many times, but it has the same results as the code above. Also tried throwing it into a var task = Task.Factory.FromAsync(ps.beginInvoke(), p => ps.EndInvoke(p)); style block and adding to a list of tasks and then awaiting all of their executions with .ConfigureAwait(false);, again... the same results!
So what are the results anyhow? The commands all execute, but on unsuccessful attempts, thee following exception is thrown:
System.Management.Automation.CmdletInvocationException: An item with the same key has already been added.
---> System.ArgumentException: An item with the same key has already been added.
Otherwise, it returns results, and very quickly.
I believe that the problem is the PowerShell session is trying to write to the same internal output object, despite the fact that it is executing as
1.) It's own new PowerShell instance
2.) In a different runspace as managedby the RunSpace pool.
This error occurs at least once every single time I run the script. Sometimes it actually spits out 8 out of 10 group results, and sometimes it only works on 1.
How can I stop this collision from happening? I am suspecting that there is something internally happening within PowerShell that is causing this to happen.
Related
We've been getting this exception intermittently in our applications:
System.IndexOutOfRangeException: Index was outside the bounds of the array.
at System.Runtime.Remoting.Channels.ChannelServices.get_RegisteredChannels()
at ProductCacheServiceClient.getProductCacheServiceProvider(DataServiceConnectionDetails connOptions)
Since it's intermittent and we call this quite a lot (in a web app, so can be concurrent) I thought maybe it was a threading issue but I've had a look in ChannelServices.get_RegisteredChannels and although this is interacting with a list of channels, it takes a copy of the reference and both RegisterChannel and UnregisterChannel appear to have locks, and replaced the list in one go.
We're stumped... If the locks are working as advertised and the MSDN docs are correct (it says static members of this class are threadsafe) then what could cause an IndexOutOfRange inside this property?
Edit: I've managed to make a small console app that will intermittently (about 1 in 5 runs in my testing) throw this exception. Im compiling in VS 2015 Update 1 targeting .NET 4.5.2.
List<Task> tasks = new List<Task>();
for (var i = 0; i < 1000; i++)
{
var task = Task.Run(() =>
{
var chanProps = new System.Collections.Hashtable();
chanProps["name"] = "cacheServicesClient" + Guid.NewGuid().ToString();
chanProps["username"] = "a";
chanProps["password"] = "b";
chanProps["domain"] = "c";
var channelToReturn = new TcpChannel(chanProps, null, null);
bool registered = false;
foreach (var channelRegistered in ChannelServices.RegisteredChannels)
{
if (channelRegistered.ChannelName == channelToReturn.ChannelName)
{
registered = true;
break;
}
}
if (!registered)
{
ChannelServices.RegisterChannel(channelToReturn, true);
}
}).ContinueWith(t =>
{
if (t.IsFaulted && t.Exception != null)
{
t.Exception.Handle(e =>
{
Console.WriteLine(e.ToString());
return true;
});
}
});
tasks.Add(task);
}
Task.WaitAll(tasks.ToArray());
and here's the error when it crashes:
System.IndexOutOfRangeException: Index was outside the bounds of the array.
at System.Runtime.Remoting.Channels.ChannelServices.get_RegisteredChannels()
at ConsoleApplication1.Program.<>c.<Main>b__0_0() in C:\Work\Source\TestApps\ConsoleApplication1\Program.cs:line 28
at System.Threading.Tasks.Task.Execute()
Press any key to continue . . .
We've confirmed in our repro that adding locks around this code fixed the problem. We believe the issue occurs when the first thread is inside ChannelServices.RegisterChannel and a second thread hits ChannelServices.RegisteredChannels at the same time. We believe it only happens on the very first hits to these (when the list is first populated with a CrossAppDomainChannel) so possible workarounds include:
Registering a single channel (to completion!) before allowing any other threads to try and create channels
Put locks around the whole chunk of code
Since our use is low volume, it's probably going to be simpler to just lock around the whole thing. Depending on your code, this may or may not be suitable!
I've got a case that might be useful to analyze and extract some conclusions.
I've got a class that implements ITaskWorker, and each Task can run simultaneously with other Task connected with a scheduling engine.
Suppose Task A runs a job for object A_1 with B1...BN attributes, while for each attribute a command line runs and gives results (which is blocked until an answer is recieved from the command line process).
This means that for Task B we can schedule the same A_1 with B1...BN attributes.
For the following piece of code and explanation, could you find something that might resolve in threads interrupting each other (deadlocks, race conditions, starvation)?
How can I ensure that there isn't a multi threaded issue here?
I think starvation cannot be an issue here, unless there are a lot of tasks of the same type that other types cannot get to be done (see below explanation about the code). I don't see a case for deadlock, but there might be a race condition on mainLocaker or connectionLockers data members (because of the same variable and collection that's are used across multiple methods).
There cannot be the same key in the dictionary (I've verified that: [b_i.A_Name + "_" + b_i.B_Name] creates a unique key)
I've got this code in C#. Please notice that mainLocker and connectorsLockers is being used in several methods like doTaskOfTypeX, so several 'types' of workers might lock it in different parts of code:
private static object mainLocker = new Object();
private static Dictionary<string, object> connectionLockers = new Dictionary<string,object>();
private doTaskOfTypeA()
{
// ... initialize A from task parameters
var attibutes = getListOfAttribuesByObject(A);
bool localLocalTaken = false;
foreach (B b_i in attibutes)
{
try{
lock (mainLocker)
{
if (!typeLockers.ContainsKey(b_i.A_Name + "_" + b_i.B_Name))
{
typeLockers.Add(b_i.A_Name + "_" + b_i.B_Name, new object());
}
}
localLocalTaken = false;
Monitor.Enter(connectionLockers[b_i.A_Name + "_" + b_i.B_Name, ref localLocalTaken);
if (localLocalTaken)
{
var calcObj = callCLIProcess(); // a CMD call is in here
if (calcObj != null)
{
// do things with calcObj
}
else
{
jobResult = new ScheduleTaskResult(ResultTypes.Failed);
}
}
}
catch
{
jobResult = new ScheduleTaskResult(ResultTypes.Failed);
throw;
}
finally
{
if (localLocalTaken)
{
Monitor.Exit(connectionLockers[b_i.A_Name + "_" + b_i.B_Name]);
}
}
}
}
Actually, there is no issue here.
The [// do things with calcObj] notation had a code from an external library that didn't work too well :-)
I'm trying to create a web app which does many things but the one that I'm currently focused in is the inbox count. I want to use EWS StreamSubscription so that I can get notification for each event and returns the total count of items in the inbox. How can I use this in terms of MVC? I did find some code from Microsoft tutorial that I was gonna test, but I just couldn't figure how I could use it in MVC world i.e. What's the model going to be, if model is the count then how does it get notified every time an event occurs in Exchange Server, etc.
Here's the code I downloaded from Microsoft, but just couldn't understand how I can convert the count to json and push it to client as soon as a new change event occurs. NOTE: This code is unchanged, so it doesn't return count, yet.
using System;
using System.Linq;
using System.Net;
using System.Threading;
using Microsoft.Exchange.WebServices.Data;
namespace StreamingNotificationsSample
{
internal class Program
{
private static AutoResetEvent _Signal;
private static ExchangeService _ExchangeService;
private static string _SynchronizationState;
private static Thread _BackroundSyncThread;
private static StreamingSubscriptionConnection CreateStreamingSubscription(ExchangeService service,
StreamingSubscription subscription)
{
var connection = new StreamingSubscriptionConnection(service, 30);
connection.AddSubscription(subscription);
connection.OnNotificationEvent += OnNotificationEvent;
connection.OnSubscriptionError += OnSubscriptionError;
connection.OnDisconnect += OnDisconnect;
connection.Open();
return connection;
}
private static void SynchronizeChangesPeriodically()
{
while (true)
{
try
{
// Get all changes from the server and process them according to the business
// rules.
SynchronizeChanges(new FolderId(WellKnownFolderName.Inbox));
}
catch (Exception ex)
{
Console.WriteLine("Failed to synchronize items. Error: {0}", ex);
}
// Since the SyncFolderItems operation is a
// rather expensive operation, only do this every 10 minutes
Thread.Sleep(TimeSpan.FromMinutes(10));
}
}
public static void SynchronizeChanges(FolderId folderId)
{
bool moreChangesAvailable;
do
{
Console.WriteLine("Synchronizing changes...");
// Get all changes since the last call. The synchronization cookie is stored in the _SynchronizationState field.
// Only the the ids are requested. Additional properties should be fetched via GetItem calls.
var changes = _ExchangeService.SyncFolderItems(folderId, PropertySet.IdOnly, null, 512,
SyncFolderItemsScope.NormalItems, _SynchronizationState);
// Update the synchronization cookie
_SynchronizationState = changes.SyncState;
// Process all changes
foreach (var itemChange in changes)
{
// This example just prints the ChangeType and ItemId to the console
// LOB application would apply business rules to each item.
Console.Out.WriteLine("ChangeType = {0}", itemChange.ChangeType);
Console.Out.WriteLine("ChangeType = {0}", itemChange.ItemId);
}
// If more changes are available, issue additional SyncFolderItems requests.
moreChangesAvailable = changes.MoreChangesAvailable;
} while (moreChangesAvailable);
}
public static void Main(string[] args)
{
// Create new exchange service binding
// Important point: Specify Exchange 2010 with SP1 as the requested version.
_ExchangeService = new ExchangeService(ExchangeVersion.Exchange2010_SP1)
{
Credentials = new NetworkCredential("user", "password"),
Url = new Uri("URL to the Exchange Web Services")
};
// Process all items in the folder on a background-thread.
// A real-world LOB application would retrieve the last synchronization state first
// and write it to the _SynchronizationState field.
_BackroundSyncThread = new Thread(SynchronizeChangesPeriodically);
_BackroundSyncThread.Start();
// Create a new subscription
var subscription = _ExchangeService.SubscribeToStreamingNotifications(new FolderId[] {WellKnownFolderName.Inbox},
EventType.NewMail);
// Create new streaming notification conection
var connection = CreateStreamingSubscription(_ExchangeService, subscription);
Console.Out.WriteLine("Subscription created.");
_Signal = new AutoResetEvent(false);
// Wait for the application to exit
_Signal.WaitOne();
// Finally, unsubscribe from the Exchange server
subscription.Unsubscribe();
// Close the connection
connection.Close();
}
private static void OnDisconnect(object sender, SubscriptionErrorEventArgs args)
{
// Cast the sender as a StreamingSubscriptionConnection object.
var connection = (StreamingSubscriptionConnection) sender;
// Ask the user if they want to reconnect or close the subscription.
Console.WriteLine("The connection has been aborted; probably because it timed out.");
Console.WriteLine("Do you want to reconnect to the subscription? Y/N");
while (true)
{
var keyInfo = Console.ReadKey(true);
{
switch (keyInfo.Key)
{
case ConsoleKey.Y:
// Reconnect the connection
connection.Open();
Console.WriteLine("Connection has been reopened.");
break;
case ConsoleKey.N:
// Signal the main thread to exit.
Console.WriteLine("Terminating.");
_Signal.Set();
break;
}
}
}
}
private static void OnNotificationEvent(object sender, NotificationEventArgs args)
{
// Extract the item ids for all NewMail Events in the list.
var newMails = from e in args.Events.OfType<ItemEvent>()
where e.EventType == EventType.NewMail
select e.ItemId;
// Note: For the sake of simplicity, error handling is ommited here.
// Just assume everything went fine
var response = _ExchangeService.BindToItems(newMails,
new PropertySet(BasePropertySet.IdOnly, ItemSchema.DateTimeReceived,
ItemSchema.Subject));
var items = response.Select(itemResponse => itemResponse.Item);
foreach (var item in items)
{
Console.Out.WriteLine("A new mail has been created. Received on {0}", item.DateTimeReceived);
Console.Out.WriteLine("Subject: {0}", item.Subject);
}
}
private static void OnSubscriptionError(object sender, SubscriptionErrorEventArgs args)
{
// Handle error conditions.
var e = args.Exception;
Console.Out.WriteLine("The following error occured:");
Console.Out.WriteLine(e.ToString());
Console.Out.WriteLine();
}
}
}
I just want to understand the basic concept as in what can be model, and where can I use other functions.
Your problem is that you are confusing a service (EWS) with your applications model. They are two different things. Your model is entirely in your control, and you can do whatever you want with it. EWS is outside of your control, and is merely a service you call to get data.
In your controller, you call the EWS service and get the count. Then you populate your model with that count, then in your view, you render that model property. It's really that simple.
A web page has no state. It doesn't get notified when things change. You just reload the page and get whatever the current state is (ie, whatever the current count is).
In more advanced applications, like Single Page Apps, with Ajax, you might periodically query the service in the background. Or, you might have a special notification service that uses something like SignalR to notify your SPA of a change, but these concepts are far more advanced than you currently are. You should probably develop your app as a simple stateless app first, then improve it to add ajax functionality or what not once you have a better grasp of things.
That's a very broad question without a clear-cut answer. Your model could certainly have a "Count" property that you could update. The sample code you found would likely be used by your controller.
My app checks at startup if any other instance of the same is running already, if yes then it will close all other instances. For this I tried using Process.GetProcessByName("AppName") function and store all the process with AppName in processes[] array. Now i want to find the PID of current instance so that i can close all other instances of my app (which obviously have same name but different PIDs). But i am unable to find that even after lot of googling. Also how can i find the PID of an instance of my app which i have created with Process.Start("AppName.exe") function called from inside AppName.exe
OK, given problems with my other solution, see the following
In order to hook in between processes, you need some form of IPC. To use the simplicty of shared handles between EventWaitHandles, you could make each program listen for a cancellation flag.
public static EventWaitHAndle CancellationEvent =
new EventWaitHandle(
false,
EventResetMode.AutoReset,
"MyAppCancel");
private object lockObject = new object();
And later...
Task.Run(() =>
{
while(true)
{
CancellationEvent.WaitOne();
lock(lockObject)
if(!thisIsCalling) // static bool to prevent this program from ending itself
Environment.Exit(0);
}
}
And then call the cancellation like so
lock(lockObject)
{
thisIsCalling = true;
CancellationEvent.Set();
thisIsCalling = false;
}
Why don't you just check equality with your current process?
var processes = Process.GetProcessByName("AppName");
foreach (var p in processes)
{
if (p != Process.GetCurrentProcess())
p.CloseMainWindow();
}
If you're interested in closing other instances of your app, why not do the opposite and prevent multiple instances from opening in the first place? Using EventWaitHandle can do this thusly:
bool created;
var eve = new System.Threading.EventWaitHandle(
false,
EventResetMode.AutoReset,
"MyAppHandle",
out created);
if(!created)
{
eve.Set();
Environment.Exit(-1); // Always use an exit error code if you're expecting to call from the console!
}
The handle parameter, "MyAppHandle" in this case, will be shared across the entire system, thus meaning not only will the out created paramete be false on secondary instaces, but you can use eve.Set() to cause the handle to fire acorss application. Set up a listening thread and this can allow a message loop to display a message when you attempt to open second instance.
Task.Run(() =>
{
while(true)
{
eve.WaitOne();
// Display an error here
}
}
I'm having a problem with my Windows Phone 8 weather app's background agent.
Whenever the background agent is run, a new http weather request is made when certain conditions (that are not relevant to the problem I'm having) are met. When these conditions are unmet, cached weather data is used instead.
Furthermore, if you have set your live tile's location to your "Current Location", the background agent will use reverse geocoding to determine the name of the area for the location you're currently at. This is done whether new or cached data is used i.e. every time my app's background agent is run.
The problem I'm having is that whenever cached data is used, the live tile is not updating. But it doesn't appear to cause an exception to occur because the app's background agent never gets blocked, even if there's been more than two times where the live tile fails to update.
This is the relevant excerpt from the background agent's view model's "public async Task getWeatherForTileLocation()" method that's called from the scheduled agent:
Scheduled agent excerpt:
protected async override void OnInvoke(ScheduledTask task)
{
LiveTileViewModel viewModel = new LiveTileViewModel();
await viewModel.getWeatherForTileLocation();
// Etc.
}
getWeatherForTileLocation() excerpt:
// If the default location is 'Current Location', then update its coordinates.
if ((int)IsolatedStorageSettings.ApplicationSettings["LocationDefaultId"] == 1)
{
try
{
// Get new coordinates for current location.
await this.setCoordinates();;
}
catch (Exception e)
{
}
}
// Depending on the time now, since last update (and many other factors),
// must decide whether to use cached data or fresh data
if (this.useCachedData(timeNow, timeLastUpdated))
{
this.ExtractCachedData(); // This method works absolutely fine, trust me. But the live tile never updates when it's run outside debugging.
// Not because of what it does, but because of how fast it executes.
}
else
{
// a httpClient.GetAsync() call is made here that also works fine.
}
The setCoordinates method, as well the reverse geocoding related methods that are called from it:
public async Task<string> setCoordinates()
{
// Need to initialise the tracking mechanism.
Geolocator geolocator = new Geolocator();
// Location services are off.
// Get out - don't do anything.
if (geolocator.LocationStatus == PositionStatus.Disabled)
{
return "gps off";
}
// Location services are on.
// Proceed with obtaining longitude + latitude.
else
{
// Setup the desired accuracy in meters for data returned from the location service.
geolocator.DesiredAccuracyInMeters = 50;
try
{
// Taken from: http://bernhardelbl.wordpress.com/2013/11/26/geolocator-getgeopositionasync-with-correct-timeout/
// Because sometimes GetGeopositionAsync does not return. So you need to add a timeout procedure by your self.
// get the async task
var asyncResult = geolocator.GetGeopositionAsync();
var task = asyncResult.AsTask();
// add a race condition - task vs timeout task
var readyTask = await Task.WhenAny(task, Task.Delay(10000));
if (readyTask != task) // timeout wins
{
return "error";
}
// position found within timeout
Geoposition geoposition = await task;
// Retrieve latitude and longitude.
this._currentLocationLatitude = Convert.ToDouble(geoposition.Coordinate.Latitude.ToString("0.0000000000000"));
this._currentLocationLongitude = Convert.ToDouble(geoposition.Coordinate.Longitude.ToString("0.0000000000000"));
// Reverse geocoding to get your current location's name.
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
this.setCurrentLocationName();
});
return "success";
}
// If there's an error, may be because the ID_CAP_LOCATION in the app manifest wasn't include.
// Alternatively, may be because the user hasn't turned on the Location Services.
catch (Exception ex)
{
if ((uint)ex.HResult == 0x80004004)
{
return "gps off";
}
else
{
// Something else happened during the acquisition of the location.
// Return generic error message.
return "error";
}
}
}
}
/**
* Gets the name of the current location through reverse geocoding.
**/
public void setCurrentLocationName()
{
// Must perform reverse geocoding i.e. get location from latitude/longitude.
ReverseGeocodeQuery query = new ReverseGeocodeQuery()
{
GeoCoordinate = new GeoCoordinate(this._currentLocationLatitude, this._currentLocationLongitude)
};
query.QueryCompleted += query_QueryCompleted;
query.QueryAsync();
}
/**
* Event called when the reverse geocode call returns a location result.
**/
void query_QueryCompleted(object sender, QueryCompletedEventArgs<IList<MapLocation>> e)
{
foreach (var item in e.Result)
{
if (!item.Information.Address.District.Equals(""))
this._currentLocation = item.Information.Address.District;
else
this._currentLocation = item.Information.Address.City;
try
{
IsolatedStorageSettings.ApplicationSettings["LiveTileLocation"] = this._currentLocation;
IsolatedStorageSettings.ApplicationSettings.Save();
break;
}
catch (Exception ee)
{
//Console.WriteLine(ee);
}
}
}
I've debugged the code many times, and have found no problems when I have. The http request when called is good, cached data extraction is good, reverse geocoding does always return a location (eventually).
But I did notice that when I'm using cached data, the name of the current location is retrieved AFTER the scheduled task has created the updated live tile but before the scheduled task has finished.
That is, the name of the location is retrieved after this code in the scheduled agent is run:
extendedData.WideVisualElement = new LiveTileWideFront_Alternative()
{
Icon = viewModel.Location.Hourly.Data[0].Icon,
Temperature = viewModel.Location.Hourly.Data[0].Temperature,
Time = viewModel.Location.Hourly.Data[0].TimeFull.ToUpper(),
Summary = viewModel.Location.Hourly.Data[0].Summary + ". Feels like " + viewModel.Location.Hourly.Data[0].ApparentTemperature + ".",
Location = IsolatedStorageSettings.ApplicationSettings["LiveTileLocation"].ToString().ToUpper(),
PrecipProbability = viewModel.Location.Hourly.Data[0].PrecipProbabilityInt
};
But before:
foreach (ShellTile tile in ShellTile.ActiveTiles)
{
LiveTileHelper.UpdateTile(tile, extendedData);
break;
}
NotifyComplete();
Obviously due to memory constraints I can't create an updated visual element at this point.
For comparison, when I'm not using cached data, the reverse geocoding query always manages to return a location before the http request code has finished.
So as the view model's getWeatherForTileLocation() method is using "await" in the scheduled agent, I decided to make sure that the method doesn't return anything until the current location's name has been retrieved. I added a simple while loop to the method's footer that would only terminate after the _currentLocation field has received a value i.e. the reverse geocoding has completed:
// Keep looping until the reverse geocoding has given your current location a name.
while( this._currentLocation == null )
{
}
// You can exit the method now, as you can create an updated live tile with your current location's name now.
return true;
When I debugged, I think this loop ran around 3 million iterations (a very big number anyway). But this hack (I don't know how else to describe it) seemed to work when I'm debugging. That is, when the target of my build was my Lumia 1020, and when I created a live tile fresh from it, which calls:
ScheduledActionService.Add(periodicTask);
ScheduledActionService.LaunchForTest(periodicTaskName, TimeSpan.FromSeconds(1));
To ensure I don't have to wait for the first scheduled task. When I debugged this first scheduled task, everything works fine: 1) a reverse geocoding request is made, 2) cached data extracted correctly, 3) hacky while loop keeps iterating, 4) stops when the reverse geocoding has returned a location name, 5) tile gets updated successfully.
But subsequent background agent calls that do use cached data don't appear to update the tile. It's only when non-cached data is used that the live tile updates. I should remind you at this point the reverse geocoding query always manages to return a location before the http request code has finished i.e. the hacky loop iterates only once.
Any ideas on what I need to do in order to ensure that the live tile updates correctly when cached data is used (read: when the processing of data, after the reverse geocoding query is made, is much faster than a http request)? Also, is there a more elegant way to stop the getWeatherForTileLocation() from exiting than my while loop? I'm sure there is!
Sorry for the long post but wanted to be as thorough as possible!
This has been giving me sleepless nights (literally) for the last 72 hours, so your help and guidance would be most appreciated.
Many thanks.
Bardi
You have done a great job of providing lots of detail, but it is very disconnected so it is a litle hard to follow. I think the root of your problem is the following:
// Reverse geocoding to get your current location's name.
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
this.setCurrentLocationName();
});
You are attempting to get the location name, but your setCoordinates method will have already completed by the time the setCurrentLocationName method gets around to executing.
Now because you need to be in a UI thread to do any tile updating anyways, I would suggest just dispatching from the begining:
protected async override void OnInvoke(ScheduledTask task)
{
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
LiveTileViewModel viewModel = new LiveTileViewModel();
await viewModel.getWeatherForTileLocation();
}
}
This would remove the need to do any other dispatching in the future.
Two more things:
Generally weather data includes the name of the location you are getting data for. If this is the case, just use that data rather than doing the reverse geocode. This will save you some memory and save time.
If you do need to get the location, I might suggest pulling out a "LocationService" that can get data for you. In this class you can use a TaskCompltionSource to await the event rather than having code follow many different paths.
public class LocationService
{
public static Task<Location> ReverseGeocode(double lat, double lon)
{
TaskCompletionSource<Location> completionSource = new TaskCompletionSource<Location>();
var geocodeQuery = new ReverseGeocodeQuery();
geocodeQuery.GeoCoordinate = new GeoCoordinate(lat, lon);
EventHandler<QueryCompletedEventArgs<IList<MapLocation>>> query = null;
query = (sender, args) =>
{
geocodeQuery.QueryCompleted -= query;
MapLocation mapLocation = args.Result.FirstOrDefault();
var location = Location.FromMapLocation(mapLocation);
completionSource.SetResult(location);
};
geocodeQuery.QueryCompleted += query;
geocodeQuery.QueryAsync();
}
return completionSource.Task;
}
Using a TaskCometionSource allows you to await the method rather than using the event.
var location = await locationService.ReverseGeocode(lat, lon);
This example uses another Location class that I created do just hold things like City and State.
The key thing with background agents is to ensure that code always flows "synchronously". This doesn't mean code cannot be asynchronous, but does mean that code needs to be called one after the other. So if you have something that has events, you could continue all other code after the event.
Hope that helps!
I don't see your deferral call. When you use async you have to tell the task that you're deferring completion till later. Can't remember the method off the top of my head but it's either on the base class of your background task or on the parameter you get.
The reason it probably works with cache data is that it probably isn't actually an async operation.
I think this is sorted now! Thanks so much Shawn for the help. The setLocationName() method call is now awaited, and it looks like this now:
public Task<string> setLocationName()
{
var reverseGeocode = new ReverseGeocodeQuery();
reverseGeocode.GeoCoordinate = new System.Device.Location.GeoCoordinate(this._currentLocationLatitude, this._currentLocationLongitude );
var tcs = new TaskCompletionSource<string>();
EventHandler<QueryCompletedEventArgs<System.Collections.Generic.IList<MapLocation>>> handler = null;
handler = (sender, args) =>
{
MapLocation mapLocation = args.Result.FirstOrDefault();
string l;
if (!mapLocation.Information.Address.District.Equals(""))
l = mapLocation.Information.Address.District;
else
l = mapLocation.Information.Address.City;
try
{
System.DateTime t = System.DateTime.UtcNow.AddHours(1.0);
if (t.Minute < 10)
IsolatedStorageSettings.ApplicationSettings["LiveTileLocation"] = l + " " + t.Hour + ":0" + t.Minute;
else
IsolatedStorageSettings.ApplicationSettings["LiveTileLocation"] = l + " " + t.Hour + ":" + t.Minute;
IsolatedStorageSettings.ApplicationSettings.Save();
this._currentLocationName = IsolatedStorageSettings.ApplicationSettings["LiveTileLocation"].ToString();
}
catch (Exception ee)
{
//Console.WriteLine(ee);
}
reverseGeocode.QueryCompleted -= handler;
tcs.SetResult(l);
};
reverseGeocode.QueryCompleted += handler;
reverseGeocode.QueryAsync();
return tcs.Task;
}