Обратное геокодирование в моем фоновом агенте заставляет мою живую плитку не обновляться, когда время обработки остальной части кода, вызываемого агентом, быстро

У меня проблема с фоновым агентом моего приложения для Windows Phone 8.

Всякий раз, когда запускается фоновый агент, создается новый запрос погоды http, когда выполняются определенные условия (не относящиеся к моей проблеме). Когда эти условия не выполняются, вместо этого используются кэшированные данные о погоде.

Кроме того, если вы установили местоположение своей текущей плитки в "Текущее местоположение", фоновый агент будет использовать обратное геокодирование, чтобы определить название области для местоположения, в котором вы находитесь в данный момент. Это делается независимо от того, используются ли новые или кэшированные данные, т.е. каждый раз, когда запускается фоновый агент моего приложения.

Проблема у меня заключается в том, что всякий раз, когда используются кэшированные данные, живая плитка не обновляется. Но, похоже, это не вызывает исключение, потому что фоновый агент приложения никогда не блокируется, даже если бы существующая плитка не обновлялась более двух раз.

Это релевантная выдержка из метода "public async Task getWeatherForTileLocation()" модели фонового агента, который вызывается из запланированного агента:

Запланированный отрывок агента:

protected async override void OnInvoke(ScheduledTask task)
{
    LiveTileViewModel viewModel = new LiveTileViewModel();
    await viewModel.getWeatherForTileLocation();

    // Etc.
}

getWeatherForTileLocation () отрывок:

// 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.
}

Метод setCoordinates, а также методы обратного геокодирования, вызываемые из него:

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);
        }
    }
}

Я много раз отлаживал код и не нашел проблем, когда у меня есть. Хороший http-запрос при вызове, извлечение кэшированных данных - это хорошо, обратное геокодирование всегда возвращает местоположение (в конце концов).

Но я заметил, что когда я использую кэшированные данные, имя текущего местоположения извлекается ПОСЛЕ того, как запланированное задание создало обновленную живую плитку, но до завершения запланированного задания.

То есть имя местоположения извлекается после запуска этого кода в запланированном агенте:

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
};

Но прежде:

foreach (ShellTile tile in ShellTile.ActiveTiles)
{
    LiveTileHelper.UpdateTile(tile, extendedData);
    break;
}

NotifyComplete();

Очевидно, из-за ограничений памяти я не могу создать обновленный визуальный элемент в этой точке.

Для сравнения, когда я не использую кэшированные данные, обратному геокодированию всегда удается вернуть местоположение до завершения кода http-запроса.

Так как метод getWeatherForTileLocation () модели представления использует "await" в запланированном агенте, я решил убедиться, что метод не возвращает ничего, пока не будет получено имя текущего местоположения. Я добавил простой цикл while в нижний колонтитул метода, который завершится только после того, как поле _currentLocation получит значение, т.е. обратное геокодирование завершено:

// 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;

Когда я отлаживал, я думаю, что этот цикл выполнял около 3 миллионов итераций (очень большое число в любом случае). Но этот хак (я не знаю, как еще описать это), казалось, работал, когда я отлаживал. То есть, когда целью моей сборки был мой Lumia 1020, и когда я создал свежую плитку из нее, которая вызывает:

ScheduledActionService.Add(periodicTask);
ScheduledActionService.LaunchForTest(periodicTaskName, TimeSpan.FromSeconds(1)); 

Чтобы убедиться, что мне не нужно ждать первого запланированного задания. Когда я отлаживал эту первую запланированную задачу, все работает нормально: 1) сделан запрос обратного геокодирования, 2) кэшированные данные извлечены правильно, 3) хакерский цикл while продолжает повторяться, 4) останавливается, когда обратное геокодирование возвращает имя местоположения, 5) плитка обновляется успешно.

Но последующие вызовы фоновых агентов, которые используют кэшированные данные, не появляются для обновления плитки. Только когда используются некэшированные данные, обновляется активная плитка. Напомню, что на этом этапе запросу обратного геокодирования всегда удается вернуть местоположение до завершения кода http-запроса, т.е. хакерский цикл повторяется только один раз.

Любые идеи о том, что мне нужно сделать, чтобы обеспечить правильное обновление живой плитки при использовании кэшированных данных (читай: когда обработка данных после выполнения запроса обратного геокодирования выполняется намного быстрее, чем запрос http)? Кроме того, есть ли более элегантный способ остановить выход getWeatherForTileLocation (), чем мой цикл while? Я уверен, что есть!

Извините за длинный пост, но хотел быть максимально тщательным!

Это давало мне бессонные ночи (буквально) в течение последних 72 часов, так что ваша помощь и руководство были бы очень признательны.

Большое спасибо.

Барди

3 ответа

Решение

Вы проделали большую работу по предоставлению большого количества деталей, но они очень отсоединены, поэтому за ними трудно следить. Я думаю, корень вашей проблемы заключается в следующем:

// Reverse geocoding to get your current location's name.
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
    this.setCurrentLocationName();
});

Вы пытаетесь получить имя местоположения, но ваш метод setCoordinates уже будет завершен к тому времени, когда метод setCurrentLocationName приступит к выполнению.

Теперь, так как вам нужно быть в потоке пользовательского интерфейса, чтобы в любом случае обновлять плитку, я бы предложил начать с самого начала:

protected async override void OnInvoke(ScheduledTask task)
{
    Deployment.Current.Dispatcher.BeginInvoke(() =>
    {
        LiveTileViewModel viewModel = new LiveTileViewModel();
        await viewModel.getWeatherForTileLocation();
    }
}

Это избавило бы от необходимости делать любую другую диспетчеризацию в будущем.

Еще две вещи:

Обычно данные о погоде включают название местоположения, для которого вы получаете данные. Если это так, просто используйте эти данные, а не обратный геокод. Это сэкономит вам память и сэкономит время.

Если вам нужно получить местоположение, я мог бы предложить вывести "LocationService", который может получить данные для вас. В этом классе вы можете использовать TaskCompltionSource для ожидания события, а не для того, чтобы код шел по разным путям.

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;
}

Использование TaskCometionSource позволяет вам ожидать метода, а не использовать событие.

var location = await locationService.ReverseGeocode(lat, lon);

Этот пример использует другой Location класс, который я создал, просто содержит такие вещи, как город и штат.

Ключевым моментом с фоновыми агентами является обеспечение того, чтобы код всегда передавался "синхронно". Это не означает, что код не может быть асинхронным, но означает, что код должен вызываться один за другим. Поэтому, если у вас есть что-то, что имеет события, вы можете продолжить весь другой код после события.

Надеюсь, это поможет!

Я не вижу твоего отсрочки. Когда вы используете асинхронный режим, вы должны сообщить задаче о том, что откладываете выполнение до более поздней версии Не могу вспомнить метод из головы, но он либо в базовом классе вашей фоновой задачи, либо в параметре, который вы получаете. Причина, по которой он, вероятно, работает с данными кэша, заключается в том, что он, вероятно, на самом деле не является асинхронной операцией.

Я думаю, что это отсортировано сейчас! Большое спасибо Шон за помощь. Теперь ожидается вызов метода setLocationName(), и теперь он выглядит так:

    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;
    }
Другие вопросы по тегам