Jump to content

SDK Wizard View


bakes82

Recommended Posts

bakes82

Whens the SDK documentation going to be updated to provide examples beyond the basics? @softworkz

Wheres the wizard view documentation?

Wheres the custom saving documentation?

Wheres the info on custom buttons/events?

Link to comment
Share on other sites

On 4/3/2024 at 5:48 AM, bakes82 said:

Whens the SDK documentation going to be updated to provide examples beyond the basics?

I had started an example a while ago, I'll try to complete it and add it to the beta SDK shortly.

On 4/3/2024 at 5:48 AM, bakes82 said:

Wheres the wizard view documentation?

Emby SDK Reference: IPluginWizardView

On 4/3/2024 at 5:48 AM, bakes82 said:

Wheres the custom saving documentation?

Custom saving means you do the saving on your own instead of saving the UI class directly.
So, basically this means that you create your own model for the configuration which you persist to disk (or elsewhere) using a serialization technique of your choice.
Once you need the data you do the loading on your own. Once you need the data for displaying UI, you load your persisted data model and use it to set the properties of your UI class.
For saving, you do it the other way round.

On 4/3/2024 at 5:48 AM, bakes82 said:

Wheres the info on custom buttons/events?

The models are documented, e.g. for ButtonItem: Emby SDK Reference: EditorTypes.ButtonItem

I'll try to include an example in the advanced sample code mentioned above.

  • Like 1
Link to comment
Share on other sites

bakes82

IIRC doing nested objects in the UI class save isn't supported, hence why I'm mentioning it as its not called out.

Dialog, and Tabbed examples would also be useful as I believe they are also sample sln, just not implemented.

This is the example for the save I got someplace.

public class PluginOptionsStore : SimpleFileStore<PluginUIOptions>
{
    public PluginOptionsStore(IApplicationHost applicationHost, ILogger logger, string pluginFullName) : base(applicationHost, logger, pluginFullName)
    {
    }
}


public class SimpleFileStore<TOptionType> : SimpleContentStore<TOptionType> where TOptionType : EditableOptionsBase, new()
{
    private readonly IFileSystem     _fileSystem;
    private readonly IJsonSerializer _jsonSerializer;
    private readonly object          _lockObj = new object();
    private readonly ILogger         _logger;
    private readonly string          _pluginConfigPath;
    private readonly string          _pluginFullName;

    private TOptionType _options;

    public SimpleFileStore(IApplicationHost applicationHost, ILogger logger, string pluginFullName)
    {
        _logger         = logger;
        _pluginFullName = pluginFullName;
        _jsonSerializer = applicationHost.Resolve<IJsonSerializer>();
        _fileSystem     = applicationHost.Resolve<IFileSystem>();

        var applicationPaths = applicationHost.Resolve<IApplicationPaths>();
        _pluginConfigPath = applicationPaths.PluginConfigurationsPath;

        if (!_fileSystem.DirectoryExists(_pluginConfigPath)) _fileSystem.CreateDirectory(_pluginConfigPath);

        OptionsFileName = $"{pluginFullName}.json";
    }

    public virtual string OptionsFileName { get; }

    public string OptionsFilePath => Path.Combine(_pluginConfigPath, OptionsFileName);

    public event EventHandler<FileSavingEventArgs> FileSaving;

    public event EventHandler<FileSavedEventArgs> FileSaved;

    public override TOptionType GetOptions()
    {
        lock (_lockObj)
        {
            if (_options == null) return ReloadOptions();

            return _options;
        }
    }

    public TOptionType ReloadOptions()
    {
        lock (_lockObj)
        {
            var tempOptions = _options ?? new TOptionType();

            try
            {
                if (!_fileSystem.FileExists(OptionsFilePath)) return tempOptions;

                using (var stream = _fileSystem.OpenRead(OptionsFilePath))
                {
                    var deserialized = tempOptions.DeserializeFromJsonStream(stream, _jsonSerializer);

                    _options = deserialized as TOptionType;
                }
            }
            catch (Exception ex)
            {
                _logger.ErrorException("Error loading plugin _options for {0} from {1}", ex, _pluginFullName, OptionsFilePath);
                _options = tempOptions;
            }

            return _options ?? new TOptionType();
        }
    }

    public override void SetOptions(TOptionType newOptions)
    {
        if (newOptions == null) throw new ArgumentNullException(nameof(newOptions));

        var savingArgs = new FileSavingEventArgs(newOptions);
        FileSaving?.Invoke(this, savingArgs);

        if (savingArgs.Cancel) return;

        lock (_lockObj)
        {
            using (var stream = _fileSystem.GetFileStream(OptionsFilePath, FileOpenMode.Create, FileAccessMode.Write))
            {
                _jsonSerializer.SerializeToStream(newOptions, stream, new JsonSerializerOptions
                                                                      {
                                                                          Indent = true
                                                                      });
            }
        }

        lock (_lockObj)
        {
            _options = newOptions;
        }

        var savedArgs = new FileSavedEventArgs(newOptions);
        FileSaved?.Invoke(this, savedArgs);
    }
}
  

 

Link to comment
Share on other sites

1 hour ago, bakes82 said:

IIRC doing nested objects in the UI class save isn't supported, hence why I'm mentioning it as its not called out.

Yes, it's correct that this isn't supported directly. I should mention in this context that the idea of serialized saving of UI classes hasn't been part of the original concept of GenericEdit (the basis for having settings UI defined by C# classes without needing to write html/js code) at all. We just recognized at a later time that serialized saving of a UI class allows for an extremely easy way for developers to have a plugin configuration plus persistence model all at once with minimal code. It's a good fit for many (simple) cases but not for all cases, and your case just goes beyond those simple cases.

So, why not add support for it now or later? 
I had surely thought about it, but the problem is that there's no one-fits-all solution. There are many different ways for how such a "one-to-many" relation can be represented in the UI:

  • A selection list
    Which renders to an HTML SELECT element, either as dropdown or a multiline select
    There might be buttons for add, edit delete
  • A plaintext section with line breaks
    A button aside would bring you to another page or dialog to configure the items
  • A GenericList
    Which renders to what you can see in the existing sample. List can be nested, have 1 or 2 buttons, a toggle or a progress bar and each item can have subitems
  • A number of repeated child UI sections (Emby SDK Reference: EditableObjectCollection). 

Each of those case is represented in the UI class in an entirely different way and that's the reason why there's no point in standardizing this for saving it the "super-easy" way.

But let's get to a practical recipe for cases like yours..

  • Like 1
Link to comment
Share on other sites

Step 1

Create a separate persistence model by using simple/poco classes like this:

    public class MyConfiguration
    {
        public string Property1 { get; set; }

        public string Property2 { get; set; }

        public IList<MyConfigDetailItem> Details { get; set; } = new List<MyConfigDetailItem>();
    }

    public class MyConfigDetailItem
    {
        public string DetailProperty1 { get; set; }

        public string DetailProperty2 { get; set; }

    }

 

Step 2

Implement loading and saving of your configuration data. Common options are:

Step 3

Remove the loading/saving of the UI class which you currently have.Instead:

  • When the config UI is to be displayed..
    • load your configuration model from Step1 using the procedure from Step2
    • Set the properties of the UI class using the values from the configuration model
    • How you add the detail items depends on your chosen way of presentation 
      (one of the ways laid out in my previous post)
    • The UI will be shown
  • When the user saves the configuration from the UI
    • load your configuration model from Step1 using the procedure from Step2
      (or use a static instance you may have in memory)
    • set the properties of your configuration model to the values of the UI model
    • save your configuration model to disk using the procedure from Step2

Summing Up

This is clearly more involved than the "easy" method, but it provides flexibility and independence between the UI model and the storage model.

I'm always doing it this way.

  • Like 1
Link to comment
Share on other sites

bakes82

Added your "preferred" saving approach to the example and I'll use as a best practice.

Im sure there is something that could be done with attributes to facilitate easier saving/mapping to some things but there is a reason I'm not in front end design/development and thankfully alot of software is now SAAS based.

Link to comment
Share on other sites

21 minutes ago, bakes82 said:

Im sure there is something that could be done with attributes to facilitate easier saving/mapping to some things

Of course it could be done - there are many ways. But it would lead to problems as soon as you are evolving your code/plugins: You might want to change the UI but when you do so, it might no longer be compatible with your previous version, but users will have saved the configuration like that already and after your update, they might no longer be able to load it without erroring. Effectively, their configurations could get lost or invalid. 
Anyway, this is a small scope with a small audience,  and not  a common framework, so they required effort needs to be put into relation to the potential gains when doing something.

If you really want it to work like that, there's another way: Custom Serialization
With the ServiceStack JSON serializer, (that Emby is using) it's possible to add two methods to a c# class which allow you to manually serialize/de-serialize as class or certain parts of it. . This way, you could still use the UI classes for presentation and persistence and manually handle only that part where multiple child configurations need to be saved and loaded.

 

43 minutes ago, bakes82 said:

thankfully alot of software is now SAAS based.

Whether software is provided as a service or via classic licensing models doesn't change one thing at all: The software always needs to be developed first 
Also, SAAL doesn't change the need for graphical user interfaces. 

 

Link to comment
Share on other sites

5 minutes ago, softworkz said:

If you really want it to work like that, there's another way: Custom Serialization
With the ServiceStack JSON serializer, (that Emby is using) it's possible to add two methods to a c# class which allow you to manually serialize/de-serialize as class or certain parts of it. . This way, you could still use the UI classes for presentation and persistence and manually handle only that part where multiple child configurations need to be saved and loaded.

Once another way would be to store the child configurations twice in the UI class. E.g. once as a "List<MyDetailConfigItem>" (for persistence) and secondly in form of an "EditableObjectCollection". Then you would just need to convert/update the collection from one property to another. Add the [Browsable(false) ] attribute to hide hide that property from the UI.

Link to comment
Share on other sites

bakes82
46 minutes ago, softworkz said:

Whether software is provided as a service or via classic licensing models doesn't change one thing at all: The software always needs to be developed first 
Also, SAAL doesn't change the need for graphical user interfaces. 

Depends on whos doing the development lol.  Im no longer a production level developer, I'm an enterprise architect so UI isnt something I deal with and when looking at SAAS software it usually means our developers arent going to be too much UI work unless we need some really custom front end thats not part of the SAAS.  We would deal more with events and data processing back to the data warehouse ;)  So while yes people developing SAAS software need to deal with this stuff the consumers of it, tend not to have to as they just add fields and what not and use the framework provided which ultimately can simplify some of the designs and also have other challenges like you say, but could be solved using a different process or UI in multi steps or what not.  Either way UI design isnt something Ive had to deal with for a few years and Im not going to complain about it.  Sometimes having the restricts force process changes for the good, maybe what you were trying to do was too complex and should be simplified.

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...