Monthly Archives: June 2015

Practical guidance on improving user experience by marshalling to UI thread just-in-time

Marshalling work between the between a background and main thread is very common pattern in many UI frameworks and Windows Presentation Foundation aka WPF is one of them. When you have a long running operation to perform, this needs to be done on a background thread, freeing the main UI thread from responding to peripheral events from mouse and keyboard operations, improving the overall user experience.

In WPF, there is one main UI thread and all objects created by the main UI thread are owned by the UI thread. Access to these objects, therefore, must be done on the main UI thread. This is well documented literature. However, what is missing is architectural guidance that one should take into consideration when designing applications that require lots of marshalling between the background and main thread. In this post I attempt this using a practical example, which involves an application that plots bus stops and associated activities on a map.

Say or application on ocassion, via a background thread calls a RESTFul web-service to retrieve spatial representation of air contaminants resulting from say a nuclear disaster within 10 km of a given position. The result of such a computation would typically look like this:

This typically is a long running operation so needs to be done in on a background thread. The result of this web-service call could be a collection of spatial records, containing bus stops including their respective coordinate and some visitation information that can be plotted onto a map.

Plotting geometries represented using vendor neutral specifications such as the Open Geospatial Consortium’s Web Feature Service specification aka OGC WFS, onto a map such as ThinkGeo’s MapSuite for WPF Desktop, or even ESRI’s ArcGIS Runtime SFK for WPF requires converting the raw data into the respective vendor’s format.

Let’s use ThinkGeo’s MapSuite as case study and take a few minutes to quickly understand their one small portion of their underlying architecture.

A ThinkGeo WpfMap object is the main UI element rendered on screen. An instance of this object exposes a collection of Overlays. There are many different types of overlays but for the sake of this discussion we limit ourselves to the LayerOverlay.

A LayerOverlay contains a collection of layers. There are many different layer types but fundamentally, the layer is responsible for converting whatever geometry context it is provided into an image that can be rendered by the Overlay. Visual representation of features can be controlled on some layers, such as the InMemoryFeatureLayer by assigning the layer an appropriate style.

Converting raw data from a vendor neutral OGC format to ThinkGeo feature representationfeature, creating the appropriate layer and styles can all be done on the background thread, since

  • these objects do not derive from DispatcherObject
  • we could be dealing with a thousand of these objects

Only after the layer has been created and we are ready to add it to the Overlays collection do we need to marshall to the UI thread. To encapsulate all of this in one place, one can implement a custom ManagedInMemoryFeatureLayer deriving from InMemoryFeatureLayer with the following logic:


    public class ManagedInMemoryFeatureLayer : InMemoryFeatureLayer
    {
        public event Action<object, EventArgs> FeaturesCreated;

        private readonly StylesFactory _stylesFactory;
        private readonly MapSuiteFeatureMapper _featureMapper;
        private readonly ILoggerFacade _logger;
        private readonly object _lock = new object();

        public ManagedInMemoryFeatureLayer(InMemoryLayerContext context, StylesFactory stylesFactory, MapSuiteFeatureMapper featureMapper)
        {
            if (!context.DataManager.IsSchemaDefined())
            {
                throw new InvalidDataException(" Geo data manager schema is not defined.");
            }
            
            Guard.ThrowIfArgumentIsNull(context.RenderInfo);
            _stylesFactory = stylesFactory;
            _featureMapper = featureMapper;

            Name = context.Name;
            SetupDataManager(context);
            
            SymbolizationInfo = context.RenderInfo;
            ApplyRenderer(stylesFactory, SymbolizationInfo, true);

            // NOTE: this line is crucial especially when you have features that need to 
            // be rendered based on the attributes.  So it is important the the geospatial 
            // schema is defined, hence the initial check.
            SetupLayerDataSchema(context.DataManager);

            // A random chosen number.
            MinNumberOfFeaturesRequiredForIndex = 20;
        }

        private void SetupDataManager(InMemoryLayerContext context)
        {
            DataManager = context.DataManager;

            DataManager.GeoDatasetChanged += (o, e) => Refresh(DataManager.GeoDataset);

            context.IsPendingLayerCreation = false;
        }

        public int MinNumberOfFeaturesRequiredForIndex { get; set; }
        public Style Style { get; set; }
        public IGeoDataManager DataManager { get; private set; }
        public ISymbolizationInfo SymbolizationInfo { get; private set; }
        public ObjectState LayerState { get; set; }
        public SpatialFilter PostFilter { get; set; }

        public void ApplyRenderer(StylesFactory styleFactory, ISymbolizationInfo stylingInfo, bool refreshLayer)
        {
            if (stylingInfo == null)
            {
                throw new ArgumentNullException("stylingInfo");
            }

            Style = styleFactory.CreateStyle(stylingInfo);

            if (stylingInfo.UseDefaultSymbolInfo)
            {
                SetDefaultStyle();
            }
            else
            {
                SetValueStyles(styleFactory, stylingInfo);
            }

            if (refreshLayer)
            {
                RecreateFeatures();
            }
        }

        private void SetupLayerDataSchema(IGeoDataManager goeDataManager)
        {
            var geoDatasetSchema = goeDataManager.GeoDataset.Schema;
            if (geoDatasetSchema != null && geoDatasetSchema.Columns.Count > 0)
            {
                Open();

                foreach (var field in geoDatasetSchema.Columns)
                {
                    if (field.ValidateColumn())
                    {
                        Columns.Add(new FeatureSourceColumn(field.Name, field.TypeString, 100));
                    }
                }

                if (SymbolizationInfo.ContainsAttibutesForStyling)
                {
                    foreach (var styleField in SymbolizationInfo.GetAttributeNamesForSpecialStyles())
                    {
                        Columns.Add(new FeatureSourceColumn(styleField));
                    }
                }

                Close();
            }
        }

        protected void SetDefaultStyle()
        {
            var oldStyle = ZoomLevelSet.ZoomLevel01.CustomStyles.FirstOrDefault(s => string.CompareOrdinal(s.Name, Style.Name) == 0);
            if (oldStyle != null)
                ZoomLevelSet.ZoomLevel01.CustomStyles.Remove(oldStyle);
            ZoomLevelSet.ZoomLevel01.CustomStyles.Add(Style);
            ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
        }

        protected void SetValueStyles(StylesFactory stylesFactory, ISymbolizationInfo renderInfo)
        {
            var attributeSymbolInfos = renderInfo.GetAttributedSymbolInfos();
            Guard.ThrowIfArgumentIsNull(attributeSymbolInfos);

            var valueStyles = stylesFactory.CreateValueStyles(attributeSymbolInfos);
            ZoomLevelSet.ZoomLevel01.CustomStyles.Clear();
            foreach (var style in valueStyles)
            {
                ZoomLevelSet.ZoomLevel01.CustomStyles.Add(style);
            }

            ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
        }

        public void RecreateFeatures()
        {
            Refresh(DataManager.GeoDataset);
        }

        public void Refresh(IGeoCollection dataset)
        {
            Task.Factory.StartNew(() =>
            {
                try
                {
                    if (PostFilter != null)
                    {
                        PostFilter.PreProcess();
                    }

                    IList<Feature> featuresToAdd = _featureMapper.CreateFeatures(dataset.GetFeatures(PostFilter)).ToArray();
                    lock (_lock)
                    {
                        InternalFeatures.Clear();
                        InternalFeatures.AddRange(featuresToAdd);
                    }

                    if (InternalFeatures.Count > MinNumberOfFeaturesRequiredForIndex)
                    {
                        BuildIndex();
                    }

                    RequestDrawing();
                    FeaturesCreated(null, null);
                }
                catch (Exception ex)
                {
                    LayerState = ObjectState.Error;
                    _logger.Error(ex, DataManager.Id.Alias);
                }
            });
        }   
        
    }
}

Creation of this class can happen on the background thread. Feature mapping can also occur on the background thread. However, when the layer calls RequestDrawing or FeatureCreated, interested parties are supposed to ensure these events are handled on the UI thread.

In our sample application, an entity called InMemoryFeatureLayerViewModel which manages a layer and its associated legend entry, a legend being the table of content representing all layers on a map, subscribes to FeaturesCreated event exposed by the layer and responds as accordingly. A snippet of a sample InMemoryFeatureLayerViewModel implementation is listed below:

 public class InMemoryFeatureLayerViewModel : FeatureLayerModel<LayerOverlay, ManagedInMemoryFeatureLayer, InMemoryLayerContext>
    {
        private readonly ManagedInMemoryFeatureLayer _layer;
        public InMemoryFeatureLayerViewModel(InMemoryLayerContext context, ManagedInMemoryFeatureLayer layer, bool isRemovable)
            : base(context)
        {
            Layer = _layer = layer;
            LayerDefinition.Layer = layer;
            LayerContext = context;

            context.RenderInfoChanged += OnRenderInfoChanged;
            _layer.FeaturesCreated += OnLayerFeaturesCreatedCreated;
        }

        private void OnRenderInfoChanged(object sender, DataEventArgs<ISymbolizationInfo> e)
        {
            // we need to execute on main UI thread...
            var dispatcher = GetMainDispatcher();

            dispatcher.BeginInvoke(new Action(() =>
            {
                // update our legend representation
                Representation = LayerContext.GetRepresentation();

                // update our layer styles and referesh layer
                _layer.ApplyRenderer(StylesFactory.Instance, e.Data, true);

            }));
        }

        private void OnLayerFeaturesCreatedCreated(object sender, EventArgs e)
        {
            _layer.FeatureSource.Open();
            var bbox = _layer.FeatureSource.GetBoundingBox();
            _layer.FeatureSource.Close();
            RefreshOverlay(bbox);
        }

   protected void RefreshOverlay(RectangleShape boundingBox)
        {
            if (Overlay == null)
                return;

            if (ParentViewModel == null)
                return;

            Dispatcher mapDispatcher = GetMainDispatcher();
            // this needs to be serviced at a high priority, so we use Normal
            mapDispatcher.BeginInvoke(new Action(() =>
            {
                //TODO: if there are performance concerns for rapidly changing layers, cache casted view.
                Overlay.PanTo(boundingBox);
                Overlay.SafelyRefresh();

                if (ParentViewModel != null)
                {
                    var map = (ExtendedWpfMap)ParentViewModel.GetMapObject();
                    if (map != null)
                    {
                        map.CurrentExtent = boundingBox;
                        map.SafelyRefresh();
                    }
                }
            }), DispatcherPriority.Normal);
        }
    }

Only when refreshing the overlay do we need to marshal to the UI thread. Otherwise, we waste UI thread cycles doing things that can be delegated to the background thread compromising application responsiveness.

One challenge when modelling your solution is reducing the number of entity definitions that rely on DispatcherObject, to just those required for rendering. This way, you can do most of the work on background threads, since these entities are thread agnostic, reserving the UI thread for rendering purposes only. This becomes especially useful, if your application comprises of multiple views rendering data grids, maps and charts. With such applications, you really want the UI thread to do nothing but render and respond to mouse and keyboard events.

Happy coding!