About multithreading
Multithreading is generally used to improve the responsiveness of applications. This responsiveness can be the result of real performance improvements or the perception of improved performance. By using multiple threads of execution in your code, you can separate data processing and input/output (I/O) operations from the management of your program's user interface (UI). This will prevent any long data processing operations from reducing the responsiveness of your UI.
The performance advantages of multithreading come at the cost of increased complexity in the design and maintenance of your code. Threads of an application share the same memory space, so you must make sure that accessing shared data structures is synchronized to prevent the application from entering into an invalid state or crashing. This synchronization is often called concurrency control.
Concurrency control can be achieved at two levels: the object level and the application level. It can be achieved at the object level when the shared object is thread safe, meaning that the object forces all threads trying to access it to wait until the current thread accessing the object is finished modifying the object’s state. Concurrency control can be done at the application level by acquiring an exclusive lock on the shared object, allowing one thread at a time to modify the object’s state. Be careful when using locks, as overuse of them will protect your data but can also lead to decreased performance. Finding a balance between performance and protection requires careful consideration of your data structures and the intended usage pattern for your extra threads.
Using multithreading
There are two items to consider when building multithreaded applications: thread safety and scalability. It is important for all objects to be thread safe, but simply having thread-safe objects does not automatically mean that creating multithreaded applications is straightforward or that the resulting application will provide improved performance.
The .NET Framework allows you to easily generate threads in your application; however, writing multithreaded ArcObjects code should be done carefully. The underlying architecture of ArcObjects is Component Object Model (COM). For that reason, writing multithreading ArcObjects applications requires an understanding of both .NET multithreading and the threading model for COM.
Multithreading does not always make your code run faster; in many cases it adds extra overhead and complexity that eventually reduces the execution speed of your code. Multithreading should only be used when the added complexity is worth the cost. A general rule of thumb is that a task is suited to multithreading if it can be broken into different independent tasks.
ArcObjects threading model
All ArcObjects components are marked as single threaded apartment (STA). STAs are limited to one thread each, but COM places no limit on the number of STAs per process. When a method call enters an STA, it is transferred to the STA's one and only thread. Consequently, an object in an STA will only receive and process one method call at a time, and every method call that it receives will arrive on the same thread.
ArcObjects components are thread safe, and you can use them in multithreaded environments. For ArcObjects applications to run efficiently in a multithreaded environment, the apartment threading model used by ArcObjects, Threads in Isolation, must be considered. This model works by eliminating cross-thread communication. All ArcObjects references within a thread should only communicate with objects in the same thread.
For this model to work, the singleton objects at ArcGIS 9.x were designed to be singletons per thread, not singletons per process. The resource overhead of hosting multiple singletons in a process is outweighed by the performance gain of stopping the cross-thread communication that would occur if the singleton were created in one thread, then accessed from the other threads. For more information, see Interacting with singleton objects.
As a developer of the extensible ArcGIS system, all objects, including those you write, must adhere to this rule for the Threads in Isolation model to work. If you are creating singleton objects as part of your development, you must ensure that these objects are singletons per thread, not per process.
To be successful using ArcObjects in a multithreaded environment, programmers must follow the Threads in Isolation model while writing their multithreaded code in such a way as to avoid application failures such as deadlock situations, negative performance due to marshalling, and other unexpected behavior.
Multithreading scenarios
Although there are a number of ways to implement multithreading applications, the following are a few of the more common scenarios that you might encounter.
The ArcObjects .NET SDK includes several threading samples, referenced in the following code examples, that cover the scenarios described in this topic. The samples demonstrate solutions for real-life problems while showing the best programming practices. While these samples use multithreading as part of a solution for a given problem, in some you will find that the multithreading is just an architectural aspect of a broader picture.
Running lengthy operations on a background thread
When you're required to run a lengthy operation, it's convenient to allow the operation to run on a background thread while leaving the application free to handle other tasks and keep the UI responsive. Some examples of such operations include iterating through a FeatureCursor to load information into a DataTable and performing a complex topological operation while writing the result into a new FeatureClass.
When running lengthy operation, keep the following in mind:
- According to the Thread in Isolation model, you cannot share ArcObjects components between threads. Instead, take advantage of the fact that singleton objects are "per thread" and, in the background thread, instantiate all factories required to open FeatureClasses, create new FeatureClasses, set spatial references, and so on.
- All information passed to the thread must be in the form of simple types or managed types.
- In cases where you must pass ArcObjects components from the main thread into a worker thread, serialize the object into a string, pass the string to the target thread, and deserialize the object back. For example, you can use XmlSerializerClass to serialize an object, such as workspace connection properties (an IPropertySet), into a string; pass the string with the connection properties to the worker thread; and deserialize the connection properties on the worker thread using the XmlSerializerClass. This way, the connection properties object are created on the background thread, and cross-apartment calls are avoided.
- While running the background thread, you can report the task progress onto a UI dialog box. This is covered in more detail in the Updating the UI from a background thread section of this topic.
The following code example demonstrates a background thread that is used to iterate through a FeatureCursor and populate a DataTable that is used later in the application. This keeps the application free to run without waiting for the table to be populated.
[C#]
// Generate the thread that populates the locations table.
Thread t = new Thread(new ThreadStart(PopulateLocationsTableProc));
// Mark the thread as a single threaded apartment (STA) to efficiently run ArcObjects.
t.SetApartmentState(ApartmentState.STA);
// Start the thread.
t.Start();
/// <summary>
/// Load the information from the MajorCities feature class to the locations table.
/// </summary>
private void PopulateLocationsTableProc()
{
//Get the ArcGIS path from the registry.
RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\ESRI\ArcGIS");
string path = Convert.ToString(key.GetValue("InstallDir"));
//Open the feature class. The workspace factory must be instantiated
//since it is a singleton per thread.
IWorkspaceFactory wf = new ShapefileWorkspaceFactoryClass()as IWorkspaceFactory;
IWorkspace ws = wf.OpenFromFile(System.IO.Path.Combine(path, @
"DeveloperKit10.0\Samples\Data\USZipCodeData"), 0);
IFeatureWorkspace fw = ws as IFeatureWorkspace;
IFeatureClass featureClass = fw.OpenFeatureClass(m_sShapefileName);
//Map the name and ZIP Code fields.
int zipIndex = featureClass.FindField("ZIP");
int nameIndex = featureClass.FindField("NAME");
string cityName;
long zip;
try
{
//Iterate through the features and add the information to the table.
IFeatureCursor fCursor = null;
fCursor = featureClass.Search(null, true);
IFeature feature = fCursor.NextFeature();
int index = 0;
while (null != feature)
{
object obj = feature.get_Value(nameIndex);
if (obj == null)
continue;
cityName = Convert.ToString(obj);
obj = feature.get_Value(zipIndex);
if (obj == null)
continue;
zip = long.Parse(Convert.ToString(obj));
if (zip <= 0)
continue;
//Add the current location to the locations table.
//m_locations is a DataTable that contains the cities and ZIP Codes.
//It is defined in the full code before this excerpt starts.
DataRow r = m_locations.Rows.Find(zip);
if (null == r)
{
r = m_locations.NewRow();
r[1] = zip;
r[2] = cityName;
lock(m_locations)
{
m_locations.Rows.Add(r);
}
}
feature = fCursor.NextFeature();
index++;
}
//Release the feature cursor.
Marshal.ReleaseComObject(fCursor);
}
catch (Exception ex)
{
System.Diagnostics.Trace.WriteLine(ex.Message);
}
}
Implementing stand-alone ArcObjects applications
As stated on the Microsoft Developer Network (MSDN) Web site, on the Thread.ApartmentState Property page, "In the .NET Framework version 2.0, new threads are initialized as ApartmentState.MTA if their apartment state has not been set before they are started. The main application thread is initialized to ApartmentState.MTA by default. You can no longer set the main application thread to ApartmentState.STA by setting the System.Threading.ApartmentState property on the first line of code. Use the STAThreadAttribute instead."
As an ArcObjects developer, this means that if your application is not initialized as a single threaded application, the .NET Framework will create a special STA thread for all ArcObjects since they are marked as STA. This will cause a thread switch to this thread on each call from the application to ArcObjects. In turn, this forces the ArcObjects components to assemble each call, and eventually it can be up to 50 times slower for a call to the COM component. Fortunately, this can be avoided by simply marking the main function as [STAThread].
The following code example marks a console application as STA:
[C#]
namespace ConsoleApplication1
{
class Program
{
[STAThread] static void Main(string[] args)
{
// ...
}
}
}
If you create a Windows Application using the project wizard, it automatically puts the [STAThread] on the main function.
Using managed ThreadPool and BackGroundWorker threads
Thread pool threads are background threads. Thread pooling enables you to use threads more efficiently by providing your application with a pool of worker threads that are managed by the system. The advantage of using a thread pool over creating a new thread for each task is that thread creation and destruction overhead is negated, which can result in better performance and better system stability.
However, by design all ThreadPool threads are in the multithreaded apartment (MTA) and therefore should not be used to run ArcObjects, which are STA.
To work around this problem, you have a few options. One is to implement a dedicated ArcObjects thread that is marked as STAThread and to delegate each of the calls from the MTA threads to this dedicated ArcObjects thread. Another solution is to use an implementation of a custom STA thread pool, such as an array of threads marked as STAThread, to run ArcObjects.
The following code example demonstrates using an array of STAThread threads to subset a RasterDataset using a different thread to subset each raster band:
[C#]
/// <summary>
/// Class used to pass the task information to the working thread.
/// </summary>
public class TaskInfo
{
... public TaskInfo(int BandID, ManualResetEvent doneEvent)
{
m_bandID = BandID;
m_doneEvent = doneEvent;
}
...
}
... public override void OnMouseDown(int Button, int Shift, int X, int Y)
{
...
// Run the subset thread that will spin off separate subset tasks.
//By default, this thread will run as MTA.
// This is needed to use WaitHandle.WaitAll(). The call must be made
// from MTA.
Thread t = new Thread(new ThreadStart(SubsetProc));
t.Start();
}
/// <summary>
/// Main subset method.
/// </summary>
private void SubsetProc()
{
...
//Create a dedicated thread for each band of the input raster.
//Create the subset threads.
Thread[] threadTask = new Thread[m_intBandCount];
//Each thread will subset a different raster band.
//All information required for subsetting the raster band will be
//passed to the task by the user-defined TaskInfo class.
for (int i = 0; i < m_intBandCount; i++)
{
TaskInfo ti = new TaskInfo(i, doneEvents[i]);
...
// Assign the subsetting thread for the rasterband.
threadTask[i] = new Thread(new ParameterizedThreadStart(SubsetRasterBand));
// Note the STA that is required to run ArcObjects.
threadTask[i].SetApartmentState(ApartmentState.STA);
threadTask[i].Name = "Subset_" + (i + 1).ToString();
// Start the task and pass the task information.
threadTask[i].Start((object)ti);
}
...
// Wait for all threads to complete their task…
WaitHandle.WaitAll(doneEvents);
...
}
/// <summary>
/// Subset task method.
/// </summary>
/// <param name="state"></param>
private void SubsetRasterBand(object state)
{
// The state object must be cast to the correct type, because the
// signature of the WaitForTimerCallback delegate specifies type
// Object.
TaskInfo ti = (TaskInfo)state;
//Deserialize the workspace connection properties.
IXMLSerializer xmlSerializer = new XMLSerializerClass();
object obj = xmlSerializer.LoadFromString(ti.InputRasterWSConnectionProps, null,
null);
IPropertySet workspaceProperties = (IPropertySet)obj;
...
}
Synchronizing execution of concurrent running threads
In many cases, you have to synchronize the execution of concurrently running threads. Normally, you have to wait for one or more threads to finish their tasks, signal a waiting thread to resume its task when a certain condition is met, test whether a given thread is alive and running, change a thread priority, or give some other indication.
In .NET, there are several ways to manage the execution of running threads. The main classes available to help thread management are as follows:
- System.Threading.Thread—Used to create and control threads, change priority, and get status.
- System.Threading.WaitHandle—Defines a signaling mechanism to indicate taking or releasing exclusive access to a shared resource, allowing you to restrict access to a block of code.
- Calling the WaitHandle.WaitAll() method must be done from an MTA thread. To run multiple synchronized tasks, you first have to run a worker thread that, in turn, will run the multiple threads.
- System.Threading.Monitor—Similar to System.Threading.WaitHandle and provides a mechanism that synchronizes access to objects.
- System.Threading.AutoResetEvent and System.Threading.ManualResetEvent—Used to notify waiting threads that an event has occurred, allowing threads to communicate with each other by signaling.
The following code example, also in the Multithreaded raster subset sample, extends what was covered in the previous section. It uses the ManualResetEvent and the WaitHandle classes to wait for multiple threads to finish their tasks. In addition, it demonstrates using the AutoResetEvent class to block running threads from accessing a block of code and to signal the next available thread when the current thread had completed its task.
[C#]
/// <summary>
/// Class used to pass the task information to the working thread.
/// </summary>
public class TaskInfo
{
//Signal the main thread that the thread has finished its task.
private ManualResetEvent m_doneEvent;
... public TaskInfo(int BandID, ManualResetEvent doneEvent)
{
m_bandID = BandID;
m_doneEvent = doneEvent;
}
... public ManualResetEvent DoneEvent
{
get
{
return m_doneEvent;
}
set
{
m_doneEvent = value;
}
}
}
//Block access to the shared resource (raster dataset).
private static AutoResetEvent m_autoEvent = new AutoResetEvent(false);
... public override void OnMouseDown(int Button, int Shift, int X, int Y)
{
...
// Run the subset thread that will spin off separate subset tasks.
// By default, this thread will run as MTA.
// This is needed because to use WaitHandle.WaitAll(), the call must be made
// from MTA.
}
/// <summary>
/// Main subset method.
/// </summary>
private void SubsetProc()
{
...
//Create ManualResetEvent to notify the main threads that
//all ThreadPool threads are done with their tasks.
ManualResetEvent[] doneEvents = new ManualResetEvent[m_intBandCount];
//Create a ThreadPool task for each band of the input raster.
//Each task will subset a different raster band.
//All information required for subsetting the raster band will be
//passed to the ThreadPool task by the user-defined TaskInfo class.
for (int i = 0; i < m_intBandCount; i++)
{
//Create the ManualResetEvent field for the task to
// signal the waiting thread that the task had been completed.
doneEvents[i] = new ManualResetEvent(false);
TaskInfo ti = new TaskInfo(i, doneEvents[i]);
...
// Assign the subsetting thread for the rasterband.
threadTask[i] = new Thread(new ParameterizedThreadStart(SubsetRasterBand));
// Note the STA, which is required to run ArcObjects.
threadTask[i].SetApartmentState(ApartmentState.STA);
threadTask[i].Name = "Subset_" + (i + 1).ToString();
// Start the task and pass the task information.
threadTask[i].Start((object)ti);
}
//Set the state of the event to signaled to allow one or more of the
//waiting threads to proceed.
m_autoEvent.Set();
// Wait for all threads to complete their task…
WaitHandle.WaitAll(doneEvents);
...
}
/// <summary>
/// Subset task method.
/// </summary>
/// <param name="state"></param>
private void SubsetRasterBand(object state)
{
// The state object must be cast to the correct type, because the signature
// of the WaitOrTimerCallback delegate specifies type Object.
TaskInfo ti = (TaskInfo)state;
...
//Lock all other threads to get exclusive access.
m_autoEvent.WaitOne();
//Insert code containing your threading logic here.
//Signal the next available thread to get write access.
m_autoEvent.Set();
//Signal the main thread that the thread has finished its task.
ti.DoneEvent.Set();
}
Sharing a managed type across multiple threads
Sometimes your .NET application’s underlying data structure is a managed object such as a DataTable or HashTable. These .NET managed objects allow you to share them across multiple threads such as a data fetching thread and a main rendering thread. However, you should consult the MSDN Web site to verify whether an item is thread safe. In many cases, an object is thread safe for reading but not for writing. Some collections implement a Synchronized method, which provides a synchronized wrapper around the underlying collection.
In cases where your object is accessed from more than one thread, you should acquire an exclusive lock according to the Thread Safety section in MSDN regarding this particular object. Acquiring such an exclusive lock can be done using one of the synchronization methods described in the previous section or by using a lock statement, which marks a block as a critical section by obtaining a mutual-exclusive lock for a given object. It ensures that if another thread attempts to access the object, it will be blocked until the object is released (exits the lock).
The following screen shot demonstrates sharing a DataTable by multiple threads. First, check the DataTable Class on the MSDN Web site to verify if it is thread safe.
On that page, check the section on Thread Safety, where it says, "This type is safe for multithreaded read operations. You must synchronize any write operations."
This means that reading information out of the DataTable is not an issue, but you must prevent other threads access to the table when you are about to write to it. The following code example shows how to block those other threads:
[C#]
private DataTable m_locations = null;
...
DataRow rec = m_locations.NewRow();
rec["ZIPCODE"] = zipCode; //ZIP Code.
rec["CITYNAME"] = cityName; //City name.
//Lock the table and add the new record.
lock(m_locations)
{
m_locations.Rows.Add(rec);
}
Updating the UI from a background thread
In most cases where you are using a background thread to perform lengthy operations, you want to report to the user the progress, status, errors, or other information related to the task performed by the thread. This can be done by updating a control on the application's UI. However, in Windows, forms controls are bound to a specific thread (generally the main thread) and are not thread safe.
As a result, you must delegate, and thus marshal, any call to a UI control to the thread to which the control belongs. The delegation is done through calling the Control.Invoke method, which executes the delegate on the thread that owns the control's underlying window handle. To verify whether a caller must call an invoke method, use the Control.InvokeRequired property. You must ensure that the control's handle was created before attempting to call Control.Invoke or Control.InvokeRequired.
The following code example demonstrates reporting a background task's progress into a user form.
- In the user form, declare a delegate through which you will pass the information to the control:
public class WeatherItemSelectionDlg: System.Windows.Forms.Form
{
private delegate void AddListItmCallback(string item);
...
- In the user form, set the method to update the UI control. Notice the call to Invoke. The method must have the same signature as the previously declared delegate:
//Make thread-safe calls to Windows Forms Controls.
private void AddListItemString(string item)
{
// InvokeRequired compares the thread ID of the
//calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if (this.lstWeatherItemNames.InvokeRequired)
{
//Call itself on the main thread.
AddListItmCallback d = new AddListItmCallback(AddListItemString);
this.Invoke(d, new object[]
{
item
}
);
}
else
{
//Guaranteed to run on the main UI thread.
this.lstWeatherItemNames.Items.Add(item);
}
}
- On the background thread, implement the method that will use the delegate and pass over the message to be displayed on the user form:
private void PopulateSubListProc()
{
//Insert code containing your threading logic here.
//Add the item to the list box.
frm.AddListItemString(data needed to update the UI control, string in this case )
;
}
- Write the call that starts the background thread itself, passing in the method written in step 3:
//Generate a thread to populate the list with items that match the selection criteria.
Thread t = new Thread(new ThreadStart(PopulateSubListProc));
t.Start();
Calling ArcObjects from a thread other than the main thread
In many multithreading applications, you will need to make calls to ArcObjects from different running threads. For example, you might have a background thread that gets a response from a Web service, which, in turn, should add a new item to the map display, change the map extent, or run a geoprocessing (GP) tool to perform some type of analysis.
A very common case is calling ArcObjects from a timer event handler method. A timer's elapsed event is raised on a ThreadPool task (a thread that is not the main thread). Yet it needs to use ArcObjects, which seems like it would require cross-apartment calls. However, this can be avoided by treating the ArcObjects component as if it were a UI control and using Invoke to delegate the call to the main thread where the ArcObjects component is created. Thus, no cross-apartment calls are made.
The ISynchronizeInvoke interface includes the Invoke, BeginInvoke, and EndInvoke methods. Implementing these methods can be a daunting task. Instead, have your class directly inherit from System.Windows.Forms.Control or have a helper class that inherits Control. Either option provides a simple and efficient solution for invoking methods.
The following code example uses a user-defined InvokeHelper class to invoke a timer’s elapsed event handler to re-center the map's visible bounds and set the map's rotation.
Some of the application logic must be done on the InvokeHelper class in addition to the user-defined structure that is being passed by the method delegate.
/// <summary>
/// A helper method used to delegate calls to the main thread.
/// </summary>
public sealed class InvokeHelper: Control
{
//Delegate used to pass the invoked method to the main thread.
public delegate void MessageHandler(NavigationData navigationData);
//Class members.
private IActiveView m_activeView;
private IPoint m_point = null;
/// <summary>
/// Class constructor.
/// </summary>
/// <param name="activeView"></param>
public InvokeHelper(IActiveView activeView)
{
//Make sure that the control was created and that it has a valid handle.
this.CreateHandle();
this.CreateControl();
//Get the active view.
m_activeView = activeView;
}
/// <summary>
/// Delegate the required method onto the main thread.
/// </summary>
/// <param name="navigationData"></param>
public void InvokeMethod(NavigationData navigationData)
{
// Invoke HandleMessage through its delegate.
if (!this.IsDisposed && this.IsHandleCreated)
Invoke(new MessageHandler(CenterMap), new object[]
{
navigationData
}
);
}
/// <summary>
/// The method that gets executed by the delegate.
/// </summary>
/// <param name="navigationData"></param>
public void CenterMap(NavigationData navigationData)
{
//Get the current map visible extent.
IEnvelope envelope =
m_activeView.ScreenDisplay.DisplayTransformation.VisibleBounds;
if (null == m_point)
{
m_point = new PointClass();
}
//Set the new map center coordinate.
m_point.PutCoords(navigationData.X, navigationData.Y);
//Center the map around the new coordinate.
envelope.CenterAt(m_point);
m_activeView.ScreenDisplay.DisplayTransformation.VisibleBounds = envelope;
//Rotate the map to the new rotation angle.
m_activeView.ScreenDisplay.DisplayTransformation.Rotation =
navigationData.Azimuth;
}
/// <summary>
/// Control initialization.
/// </summary>
private void InitializeComponent(){}
/// <summary>
/// A user-defined data structure used to pass information to the Invoke method.
/// </summary>
public struct NavigationData
{
public double X;
public double Y;
public double Azimuth;
/// <summary>
/// Struct constructor.
/// </summary>
/// <param name="x">map x coordinate</param>
/// <param name="y">map x coordinate</param>
/// <param name="azimuth">the new map azimuth</param>
public NavigationData(double x, double y, double azimuth)
{
X = x;
Y = y;
Azimuth = azimuth;
}
/// <summary>
/// This command triggers the tracking functionality.
/// </summary>
public sealed class TrackObject: BaseCommand
{
//Class members.
private IHookHelper m_hookHelper = null;
... private InvokeHelper m_invokeHelper = null;
private System.Timers.Timer m_timer = null;
...
/// <summary>
/// Occurs when this command is created.
/// </summary>
/// <param name="hook">Instance of the application</param>
public override void OnCreate(object hook)
{
...
//Instantiate the timer.
m_timer = new System.Timers.Timer(60);
m_timer.Enabled = false;
//Set the timer's elapsed event handler.
m_timer.Elapsed += new ElapsedEventHandler(OnTimerElapsed);
}
/// <summary>
/// Occurs when this command is clicked.
/// </summary>
public override void OnClick()
{
//Create the InvokeHelper class.
if (null == m_invokeHelper)
m_invokeHelper = new InvokeHelper(m_hookHelper.ActiveView);
...
//Start the timer.
if (!m_bIsRunning)
m_timer.Enabled = true;
else
m_timer.Enabled = false;
...
}
/// <summary>
/// Timer elapsed event handler.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnTimerElapsed(object sender, ElapsedEventArgs e)
{
...
//Create the navigation data structure.
NavigationData navigationData = new NavigationData(currentPoint.X,
currentPoint.Y, azimuth);
//Update the map extent and rotation.
m_invokeHelper.InvokeMethod(navigationData);
...
}
Multithreading with geoprocessing
To use geoprocessing in an asynchronous or multithreaded application, use one of the following options:
-
In ArcGIS Server 9.2 and above, use geoprocessing services. This enables the desktop to work with geoprocessing in an asynchronous execution mode and to run multiple geoprocessing tools asynchronously with each other. The mechanism to execute the server tool and listen for feedback is described in How to work with geoprocessing services.
-
In ArcGIS 10, use the Geoprocessor.ExecuteAsync method. You can execute tools asynchronously to the ArcGIS application. This means that an ArcGIS for Desktop application or control (for example, MapControl, PageLayoutControl, GlobeControl, or SceneControl) remains responsive to user interaction while a tool is executed in a background process. In other words, data can be viewed and queried while a tool is executing input datasets. This is more fully described in Running a geoprocessing tool using background geoprocessing.
See Also:
Sample: RSS weather GraphicTrackerSample: RSS weather layer
Sample: RSS weather 3D layer
Sample: Dynamic display—tracking dynamic object
Sample: Dynamic display layer
Sample: Dynamic biking