Windows Phone 8 の天気アプリのバックグラウンド エージェントに問題があります。
バックグラウンド エージェントが実行されるたびに、特定の条件 (私が抱えている問題とは無関係) が満たされると、新しい http 天気リクエストが作成されます。これらの条件が満たされない場合、代わりにキャッシュされた気象データが使用されます。
さらに、ライブ タイルの場所を「現在の場所」に設定した場合、バックグラウンド エージェントはリバース ジオコーディングを使用して、現在いる場所のエリアの名前を特定します。これは、新しいデータまたはキャッシュされたデータが使用されるかどうかにかかわらず、つまりアプリのバックグラウンド エージェントが実行されるたびに行われます。
私が抱えている問題は、キャッシュされたデータが使用されるたびに、ライブ タイルが更新されないことです。ただし、ライブ タイルの更新に 2 回以上失敗した場合でも、アプリのバックグラウンド エージェントがブロックされることはないため、例外が発生することはないようです。
これは、スケジュールされたエージェントから呼び出されるバックグラウンド エージェントのビュー モデルの「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」を使用しているため、現在の場所の名前が取得されるまでメソッドが何も返さないことを確認することにしました。_currentLocation フィールドが値を受け取った後、つまりリバース ジオコーディングが完了した後にのみ終了する単純な while ループをメソッドのフッターに追加しました。
// 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;
私がデバッグしたとき、このループは約 300 万回繰り返されたと思います (とにかく非常に大きな数です)。しかし、このハック (他にどのように説明すればよいかわかりません) は、デバッグ中に機能するように見えました。つまり、ビルドのターゲットが Lumia 1020 で、そこから新しいライブ タイルを作成したとき、次のように呼び出します。
ScheduledActionService.Add(periodicTask);
ScheduledActionService.LaunchForTest(periodicTaskName, TimeSpan.FromSeconds(1));
最初のスケジュールされたタスクを待つ必要がないようにするため。この最初のスケジュールされたタスクをデバッグしたところ、すべて正常に動作しました。1) リバース ジオコーディング リクエストが行われ、2) キャッシュされたデータが正しく抽出され、3) ループが繰り返されている間ハッキングされ、4) リバース ジオコーディングが場所の名前を返すと停止し、5 ) タイルが正常に更新されます。
ただし、キャッシュされたデータを使用する後続のバックグラウンド エージェント呼び出しでは、タイルが更新されないように見えます。ライブ タイルが更新されるのは、キャッシュされていないデータが使用されている場合のみです。この時点で、リバース ジオコーディング クエリは常に、http リクエスト コードが完了する前に場所を返すことができます。つまり、ハッキー ループは 1 回だけ繰り返されます。
キャッシュされたデータが使用されたときにライブ タイルが正しく更新されるようにするために何をする必要があるかについてのアイデアはありますか? また、 getWeatherForTileLocation() が終了するのを私の while ループよりもエレガントに停止する方法はありますか? きっとあるよ!
長い投稿で申し訳ありませんが、できるだけ徹底的にしたかったのです!
これにより、過去 72 時間、(文字通り) 眠れぬ夜が続いています。ご協力とご指導をよろしくお願いいたします。
どうもありがとう。
バルディ