We have recently uploaded the Weather Station demo showing some of Telerik controls for Silverlight in a great rich context. Following Kalin’s blog post announcing the main features, this one is to provide some notes on the technical side of the matter.
Here is a list of the main features:
It is based on the IP address of the client requesting the data. This is how to get it from the WCF service (WeatherService.svc.cs):
OperationContext context = OperationContext.Current;
MessageProperties messageProperties = context.IncomingMessageProperties;
RemoteEndpointMessageProperty endpointProperty = messageProperties[RemoteEndpointMessageProperty.Name]
as
RemoteEndpointMessageProperty;
String ipAddress = endpointProperty.Address;
Then, all that is needed is to query a the web service for the geo-location which corresponds to this IP address. Once you have it, you can make requests for weather, coordinates, etc.
What we did is basically store the locations’ names in a text format (separated by ”;”) in the Silverlight’s IsolatedStorage. The code for accessing it resides in the IsolatedStorageHelper class, and its consumption is as simple as:
Get all locations and favorites upon initial load:
string
defaultsString = IsolatedStorageHelper.GetDefaults();
…
string
favouritesString = IsolatedStorageHelper.GetFavourites();
internal
void
SaveDataState()
{
string
[] defaults =
this
.weatherInfoCache.Select(f => f.LocationInfo.FullName).ToArray<
string
>();
string
[] favs =
this
.weatherInfoCache.Where(wi=>wi.IsFavourite ==
true
).Select(f => f.LocationInfo.FullName).ToArray<
string
>();
IsolatedStorageHelper.SaveFavourites(
string
.Join(
";"
, favs));
IsolatedStorageHelper.SaveDefaults(
string
.Join(
";"
, defaults));
}
<
DataTemplate
x:Key
=
"ForecastTempCItemTemplate"
>
<
TextBlock
FontFamily
=
"/WeatherMonitor;component/Fonts/Fonts.zip#Segoe WP"
>
<
Run
Text
=
"{Binding LowC}"
/><
Run
Text
=
"°~"
/><
Run
Text
=
"{Binding HighC}"
/><
Run
Text
=
"°"
/>
</
TextBlock
>
</
DataTemplate
>
<
DataTemplate
x:Key
=
"ForecastTempFItemTemplate"
>
<
TextBlock
FontFamily
=
"/WeatherMonitor;component/Fonts/Fonts.zip#Segoe WP"
>
<
Run
Text
=
"{Binding LowF}"
/><
Run
Text
=
"°~"
/><
Run
Text
=
"{Binding HighF}"
/><
Run
Text
=
"°"
/>
</
TextBlock
>
</
DataTemplate
>
<
local:TemplateSelectorConverter
x:Key
=
"ForecastTemperatureItemTemplates"
>
<
local:TemplateMapEntry
SourceType
=
"US"
DataTemplate
=
"{StaticResource ForecastTempFItemTemplate}"
/>
<
local:TemplateMapEntry
SourceType
=
"EU"
DataTemplate
=
"{StaticResource ForecastTempCItemTemplate}"
/>
</
local:TemplateSelectorConverter
>
<
telerik:RadTransitionControl
x:Name
=
"temperatureContainer"
UseLayoutRounding
=
"False"
Duration
=
"00:00:00.5"
Content
=
"{Binding}"
ContentTemplate
=
"{Binding Tag, Converter={StaticResource ForecastTemperatureItemTemplates}, ElementName=forecastItemsControl}"
HorizontalAlignment
=
"Center"
VerticalAlignment
=
"Top"
IsHitTestVisible
=
"False"
Width
=
"64"
Foreground
=
"White"
FontWeight
=
"Bold"
FontSize
=
"16"
HorizontalContentAlignment
=
"Center"
>
<
telerik:RadTransitionControl.Effect
>
<
DropShadowEffect
BlurRadius
=
"3"
ShadowDepth
=
"2"
/>
</
telerik:RadTransitionControl.Effect
>
<
telerik:RadTransitionControl.Transition
>
<
telerik:SlideAndZoomTransition
SlideDirection
=
"RightToLeft"
MinZoom
=
"1"
/>
</
telerik:RadTransitionControl.Transition
>
</
telerik:RadTransitionControl
>
The most convenient way to implement a kind of auto-complete textbox using a RadComboBox, so that while writing we can set its Items to the suggestions coming from the web service.
<
telerik:RadComboBox
x:Name
=
"searchCombo"
Grid.Row
=
"1"
……….
IsEditable
=
"True"
Text
=
"{Binding SearchString, Mode=TwoWay}"
SelectedItem
=
"{Binding SelectedItem, Mode=TwoWay}"
ItemsSource
=
"{Binding AllSearchRegions}"
………>
What we use to trigger the search is the TwoWay binding to the SearchString property of our ViewModel – as soon as it is being set to a new value, the Search() method is called where a new BackgroundWorker is initialized and started, and an ID of the current(and last called) search operation is stored so that we can later get the latest asynchronous search result:
private
void
Search()
{
latestSearchId = Guid.NewGuid();
this
.InitializeWorker();
this
.searchWorker.RunWorkerAsync();
}
And one of the reasons why we do this in a separate thread is that we want to sleep it for a while to add a delay in the search request. Here is what the worker does:
private
void
OnSearchWorkerDoWork(
object
sender, DoWorkEventArgs e)
{
Guid thisId = latestSearchId;
Thread.Sleep(400);
Action<List<GeoLocationInformation>> onSearchComplete = (result) =>
{
if
(thisId == latestSearchId)
// this restricts setting the suggestion results only when they apply to the latest search
{
if
(result !=
null
)
{
this
.AllSearchRegions.Clear();
foreach
(var locationInfo
in
result)
{
if
(locationInfo !=
null
)
{
this
.AllSearchRegions.Add(locationInfo);
}
}
}
}
};
SearchHelper.SearchLocationWWO(
this
.SearchString, onSearchComplete);
}
As we are displaying a number of items on the map, this number may vary on the user’s preferences. Also we need the items to be virtualized so that they do not consume system resources in vain, thus slowing the performance. This suggests the use of DynamicLayer on the map with our own implementation of a DynamicSource and an ItemTemplateSelector for switching the weather information between normal and details view:
<telerik:RadMap x:Name="RadMap1" ...>
<telerik:DynamicLayer x:Name="dynamicLayer" DynamicSource="{Binding MapSource}"
ItemTemplateSelector="{StaticResource WeatherInfoItemTemplateSelector}" >
<telerik:ZoomGrid LatitudesCount="4" LongitudesCount="4" MinZoom="4" />
<telerik:ZoomGrid LatitudesCount="256" LongitudesCount="256" MinZoom="10" />
</telerik:DynamicLayer>
</telerik:RadMap>
public
override
DataTemplate SelectTemplate(
object
item, DependencyObject container)
{
switch
((container.GetVisualParent<DynamicLayer>().MapControl).ZoomLevel)
{
case
10:
case
11:
return
this
.DetailedItemTemplate;
default
:
return
this
.BasicItemTemplate;
}
}
At several places in the solution we need to call asynchronous methods in a synchronous way, so that we can report progress and also continue the execution only after we receive certain results. To do this we use AutoResetEvent objects:
private
void
LoadPredefinedGeoLocations()
{
foreach
(
string
location
in
this
.predefinedSearchLocations)
{
// use AutoResetEvent to make the method synchronous, e.g. return to calling method after loading is finished
AutoResetEvent autoResetEvent =
new
AutoResetEvent(
false
);
// this timer is to apply a timeout of the get location & weather operation
Timer t =
new
Timer(
new
TimerCallback((obj) =>
{
// give a signal
autoResetEvent.Set();
}));
t.Change(10000, Timeout.Infinite);
Action<List<GeoLocationInformation>> onSearchComplete = (result) => {
this
.RetrieveWeatherData(result.First(),
new
Action(() => {
t.Dispose();
// give a signal
autoResetEvent.Set();
}));
};
SearchHelper.SearchLocationWWO(location, onSearchComplete);
//wait for a signal before continuing
autoResetEvent.WaitOne();
}
}