Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Tip

This feature required us to add deterministic channel load order to Connexion and Remote Agent. In order to ensure that objects are registered before they are read, we have created the concept of a Non Processing Channel. A Non Processing Channel is loaded /started prior to regular channels. Read about Non Processing Channels.

...

Code Block
languagec#
public override void Load(string configuration)
{
    base.Load(configuration);
    MessageChannel.ObjectRegistryService.Register(MessageChannel.GroupKeyGroupKeyString, Configuration);
}

In the above example, a device configuration is being registered at the group level (by using the GroupKey property). This object can be read by other devices by using the Get<T>(object (string key) or GetAsync<T>(string key) method:

Code Block
breakoutModewide
languagec#
// Get<T> will throw if no object is registered under the given key
var configuration = MessageChannel.ObjectRegistryService.Get<SharedObjectType>(MessageChannel.GroupKey)GroupKeyString);

// GetAsync<T> blocks until an object with the given key is registered. In most cases, you will want the async version
var configuration = await MessageChannel.ObjectRegistryService.GetAsync<SharedObjectType>(MessageChannel.GroupKeyString);

The typical architecture for sharing device configuration is to place the configuration class into a separate shared assembly. The configuration publisher device and any consumer devices will reference this shared assembly. A sample device configuration might look as follows:

...

Code Block
languagec#
using Connexion.Core;
using Shared;
using System;

namespace ConfigOwnerDevice
{
    [DevicePlugin("Configuration Master Device", "Hosts a shared configuration", DeviceDefinitionFlags.NonProcessingDevice, typeof(object), typeof(object), typeof(SingletonTestingFactory))]
    public class ConfigOwnerDevice: BaseDevice<SharedConfiguration>
    {
        public ConfigOwnerDevice(Guid deviceKey, IMessageChannelDevice messageChannelDevice)
          : base(deviceKey, messageChannelDevice)
        {
        }

        public override void Load(string configuration)
        {
            base.Load(configuration);
            // register our configuration for other devices to read.
            MessageChannel.ObjectRegistryService.Register(MessageChannel.GroupKey, Configuration);
        }
    }
}

A device which wants to read this configuration could be coded as follows:

Code Block
breakoutModewide
languagec#
using Connexion.Core;
using Shared;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConfigurationConsumer
{
    [DevicePlugin("Configuration Consumer", "Consume configuration of a different device", DeviceDefinitionFlags.None, typeof(object), typeof(object), typeof(ConsumerFactory))]
    public class ConfigurationConsumer : BaseDevice<ConsumerConfiguration>, IConfigurationConsumer
    {// if a user updates the device configuration (via the UI), Load will be called
        private SharedConfiguration m_Configuration;  // and the sharednew configconfiguration registered (replacing the previously registered config)
  private int m_SomeCachedVal;        // sample cached propertyMessageChannel.ObjectRegistryService.Register(MessageChannel.GroupKeyString, Configuration);
        }
 private int m_ConfigurationVersion; // used to display the shared configuration in this device's UI

        public ConfigurationConsumer(Guid deviceKey, IMessageChannelDevice messageChannelDevice)
          : base(deviceKey, messageChannelDevice)
        {
            // get a handle to the shared configuration
            UpdateConfiguration();
            // get notified when the configuration is re-published
            MessageChannel.ObjectRegistryService?.RegisterForEvent<SharedConfiguration>(MessageChannel.GroupKey, ObjectRegistryService_OnRegistrationUpdated }
}

We strongly recommend you do not read from a locally cached shared object, as this has thread-safety implications. The pattern we recommend is to call Get or GetAsync every time you are about to access your shared object(s), and place that into a local variable:

Info

GetAsync<T> blocks until an object with the given key is registered. This allows the device which registers your shared object(s) to complete loading of the shared object(s) after consumer devices have started. Just remember to use CancellationToken with the GetAsync<T> methods.

Code Block
languagec#
public override async Task ProcessMessageAsync(IMessageContext context, CancellationToken token)
{
  // GetAsync blocks until the sharedObject is registered by the 'source' channel.
  // Pass in the cancellation token to ensure the object registration waiting is aborted
  // when the channel is stopped.
  var sharedObject = await MessageChannel.ObjectRegistryService.GetAsync<SharedConfiguration>(MessageChannel.GroupKeyString, token);
    
  // }... your logic that uses shared object properties ...
 private void ObjectRegistryService_OnRegistrationUpdated()
        {
            // fired when the configuration object is replaced
            UpdateConfiguration();
        }

        private void UpdateConfiguration()
        {
            // get a handle to the configuration for our group
            m_Configuration = MessageChannel.ObjectRegistryService?.Get<SharedConfiguration>(MessageChannel.GroupKey);
            if (m_Configuration == null)
                return;

       context.Keywords.Add(sharedObject.SomeInterestingProperty);
}

In some cases, you may be forced to update other objects when the shared object changes. In these cases, you can hold a reference to the ‘current’ object, and compare that against the object returned by Get / GetAsync.

Code Block
languagec#
private SharedConfiguration m_CurrentSharedObject;

public override async Task ProcessMessageAsync(IMessageContext context, CancellationToken token)
{
  // see above sample
  var sharedObject = await MessageChannel.ObjectRegistryService.GetAsync<SharedConfiguration>(MessageChannel.GroupKeyString, token);
  
  // ifis we haveour cached values,shared we should update them to the new value object different from the sharedone configurationreturned by GetAsync?
         if (!ReferenceEquals(sharedObject, m_SomeCachedValCurrentSharedObject))
= m_Configuration.ConfigurationB;             m_ConfigurationVersion++RefreshState();   // yes it is. Update logic }to act on the updated      public override async Task ProcessMessageAsync(IMessageContext context, CancellationToken token)
        {
    shared config...
    
  // ... your logic herethat uses shared object properties ...
   }

        // this method is called by the UI to display the current configuration values
        public Task<GetConfigurationResponse> GetConfigurationAsync(GetConfigurationRequest request)
       }

If you are writing a polling or source device (a device which pushes new messages onto a channel), then you can call the GetAsync<T> method at the beginning of each poll loop (or the OnScheduleTimeout if using scheduling):

Code Block
languagec#
public override void Start()
{
  StartScheduling();
}

public override void Stop()
{
  StopScheduling();
}

protected override async Task OnScheduleTimeout(OnScheduleTimeoutArgs args)
{   
  var sharedConfig =     return Task.FromResult(new GetConfigurationResponse(m_ConfigurationVersion, m_ConfigurationVersion == request.Version ? null : m_Configuration))await MessageChannel.ObjectRegistryService.GetAsync<SharedConfiguration>(MessageChannel.GroupKeyString, MessageChannel.CancellationToken);
  // .. your message creation logic }here ..
   }await MessageChannel.PostOnChannelAsync(MessageChannel.CreateMessageContext(MyMessage));
}

Download the sample device solution here.

Download the sample channels here.

If you import the sample channels into a new tab, you should have a Config Source channel and a Config Consumer channel. You’ll notice the Config Source channel has a different title color and a hatched background. This is a Non-Processing channel which is loaded before regular channels.

Info

The samples have changed slightly from the below screenshots. The intent, however, remains the same.

...

If you edit this channel, you’ll notice the Non Processing Channel checkbox is set.

...

The Shared Config Source device has a simple UI to edit a few fields as well as a map tables tab. This UI edits the configuration that is shared with other devices. The Configuration Consumer device (in the Config Consumer channel) uses code similar to the above examples (device code) to get a reference to the shared configuration. The device UI displays a json-serialized version of the configuration so we can easily visualize the consumers view of the shared configuration. This UI refreshes every 5 seconds (while the UI is visible), so we can make changes to the source configuration and then visualize that change in the consumer device.

...

Make a change to the source device UI and save the channel:

...

And then switch back to the consumer:

...

In practice, your consumer devices will probably have their own configuration and UI, and simply reference shared fields at runtime.