Telerik blogs

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.

weather-station
Here is a list of the main features:

  1. Find client’s location upon startup and retrieve weather information if available.
  2. Store current locations and favorite ones in the local storage for proper loading on next startup of the application.
  3. Use two formats for weather values - Celsius and Fahrenheit.
  4. Search for custom location.
  5. Display brief location information (on smaller zoom levels) as well as detailed view when zoomed-in to the largest level.

Find client’s location upon startup and retrieve weather information if available

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.

Store current locations and favorite ones in the local storage for proper loading on next startup of the application

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

…and we call the following method as soon as we want to store the default and favorite locations:
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));
}

Use two formats for weather values

Since our data object is of type WeatherConditions, we have 2 separate properties for Celsius and Fahrenheit (if temperature is concerned). This means that we need to have some kind of functionality that chooses between what property is currently been bound to the Content of the RadTransitionControl that is displaying it – just like a data template selector. To implement this, we have created the TemplateSelectorConverter (thank you for the idea, Rob) that, simply chooses between DataTemplates depending on a parameter that is passed (and this in our case is “US”, and “EU”, corresponding to imperial/metric values). Below is how a converter for temperature is declared:
 
<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>
 
…and this is an example of how it is set to enable a RadTransitionControl animate when we toggle between different formats (we store the IsUsingUSValues value in the Tag property of the parent control):
<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>
 

Search for custom location

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


Display brief location information (on smaller zoom levels) as well as detailed view when zoomed-in to the largest level

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>

 
Setting the different DataTemplates is as easy as:
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();
    }
}
 
 

About the Author

Teodor Bozhikov

 is Senior Software Developer in Telerik xLabs Team

Comments

Comments are disabled in preview mode.