2

Is it possible to have an object injected through the constructor and register an event on it? I am currently trying to implement something like this and my PropertyChanged even is always null. I think it has to do with the fact that I am not instantiating the object in my class where I register the event, but I am not quite sure as I am still new to event driven code.

Anyway here is the relevant code:

Injected via dependency:

public static class ServiceCollectionExtensions
{
    public static void ConfigureWritable<T>(
        this IServiceCollection services,
        IConfigurationSection section,
        string file = "appsettings.json") where T : class, new()
    {
        services.Configure<T>(section);
        services.AddTransient<IWritableOptions<T>>(provider =>
        {
            var environment = provider.GetService<IHostingEnvironment>();
            var options = provider.GetService<IOptionsMonitor<T>>();
            return new WritableOptions<T>(environment, options, section.Key, file);
        });
    }
}

public interface IWritableOptions<out T> : IOptionsSnapshot<T> where T : class, new()
{
    void Update(Action<T> applyChanges);
    event PropertyChangedEventHandler PropertyChanged;
}

public class WritableOptions<T> : INotifyPropertyChanged, IWritableOptions<T> where T : class, new()
{
    private readonly IHostingEnvironment _environment;
    private readonly IOptionsMonitor<T> _options;
    private readonly string _section;
    private readonly string _file;

    public WritableOptions(
        IHostingEnvironment environment,
        IOptionsMonitor<T> options,
        string section,
        string file)
    {
        _environment = environment;
        _options = options;
        _section = section;
        _file = file;
    }

    public T Value
    {
        get
        {
            var fileProvider = _environment.ContentRootFileProvider;
            var fileInfo = fileProvider.GetFileInfo(_file);
            var physicalPath = fileInfo.PhysicalPath;

            var jObject = JsonConvert.DeserializeObject<JObject>(File.ReadAllText(physicalPath));
            var sectionObject = jObject.TryGetValue(_section, out JToken section) ?
                JsonConvert.DeserializeObject<T>(section.ToString()) : (Value ?? new T());

            return sectionObject;
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, e);
        }
    }

    protected void OnPropertyChanged(string propertyName)
    {
        OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
    }

    public T Get(string name) => _options.Get(name);

    public void Update(Action<T> applyChanges)
    {
        var fileProvider = _environment.ContentRootFileProvider;
        var fileInfo = fileProvider.GetFileInfo(_file);
        var physicalPath = fileInfo.PhysicalPath;

        var jObject = JsonConvert.DeserializeObject<JObject>(File.ReadAllText(physicalPath));
        var sectionObject = jObject.TryGetValue(_section, out JToken section) ?
            JsonConvert.DeserializeObject<T>(section.ToString()) : (Value ?? new T());

        applyChanges(sectionObject);

        jObject[_section] = JObject.Parse(JsonConvert.SerializeObject(sectionObject));
        File.WriteAllText(physicalPath, JsonConvert.SerializeObject(jObject, Formatting.Indented));

        OnPropertyChanged("Value");
    }
}

Class consuming the injected dependency:

public class ClientService : HostedService
{
    public ClientService(IWritableOptions<ClientSettings> clients)
    {
        clients.PropertyChanged += PropertyChanged;
    }

    private void PropertyChanged(object sender, PropertyChangedEventArgs e)
    {

    }
}

EDIT: To further clarify, I all use this in a .Net Core WebAPI project. I register ClientService like this:

public void ConfigureServices(IServiceCollection services)
{
    services.ConfigureWritable<ClientSettings>(Configuration.GetSection("ClientSettings"));
    services.AddSingleton<IHostedService, ClientService>();

    services.AddMvc();
}

I then have a controller with which I can update these settings:

private IWritableOptions<ClientSettings> _clients;

public ClientController(IWritableOptions<ClientSettings> clients)
{
    _clients = clients;
}

[HttpPost]
[Route(CoreConfigClientRoutes.UpdateRoute)]
public void UpdateClients([FromBody] IEnumerable<ModuleSetting> updatedClients)
{
    var clients = _clients.Value.Clients.ToList();
    foreach (var updatedClient in updatedClients)
    {
        var index = clients.IndexOf(clients.Where(c => c.Id == updatedClient.Id).First());
        clients[index] = updatedClient;
    }
    _clients.Update(c => c.Clients = clients.ToArray());
}

Sources: IHostedService, IWritableOptions

Am I missing something here or is this not possible?

Working GitHub Repo: EventDI

LeonidasFett
  • 3,052
  • 4
  • 46
  • 76
  • it is always null when? could just be that when you call it there is nothing subscribed as yet – Nkosi Feb 28 '18 at 15:48
  • Provide more context to help us understand the problem – Nkosi Feb 28 '18 at 15:50
  • ClientService is registered in the ConfigureServices method of Startup.cs in a WebAPI. In the same WebAPI, I have a controller that can be used to update IWritableOptions. – LeonidasFett Feb 28 '18 at 15:51
  • Update the OP to include a [mcve] that we can use to reproduce the problem. – Nkosi Feb 28 '18 at 15:52
  • Ok I have updated my question. – LeonidasFett Feb 28 '18 at 15:57
  • ok reviewing. Have you stepped through this code during debugging? – Nkosi Feb 28 '18 at 16:18
  • Yes. I just added the event functionality to IWrittableOptions now, before that, it all worked fine. While debugging, I just noticed that PropertyChanged is never set, even though when I subscribe to it in ClientService. That's the part I also don't quite understand. The options should be instantiated by the framework when it's passed through the constructor, so if there is a subscriber, why is PropertyChanged null? – LeonidasFett Feb 28 '18 at 16:22
  • Consider this workaround `public event PropertyChangedEventHandler PropertyChanged = delegate { };` – Nkosi Feb 28 '18 at 16:35
  • I tried this. Although PropertyChanged(this, e) is executed, the subscribed event in ClientService is never called. – LeonidasFett Feb 28 '18 at 17:01
  • I have created a GitHub repo with a working example where my problem is reproduced. Linked it in my question. – LeonidasFett Feb 28 '18 at 17:04

1 Answers1

0

I was getting above in the WPF app.

You probably shouldn't use code below if you care. However, for a quick 'patch and move on' I did the following.

I castrated the above solution since it was failing with dependency injection bits. Just manually described where to get appsettings.json and removed the failing to bind components.

public class WritableOptions<T> : IWritableOptions<T> where T : class, new()
{
    private readonly IOptionsMonitor<T> _options;
    private readonly string _section;

    public WritableOptions(
        IOptionsMonitor<T> options,
        string section,
        string file)
    {
        _options = options;
        _section = section;
    }

    public T Value => _options.CurrentValue;
    public T Get(string name) => _options.Get(name);

    public void Update(Action<T> applyChanges)
    {
        var physicalPath = Directory.GetParent(Environment.CurrentDirectory).Parent.Parent.FullName+ "\\appsettings.json";

        var jObject = JsonConvert.DeserializeObject<JObject>(File.ReadAllText(physicalPath));
        var sectionObject = jObject.TryGetValue(_section, out JToken section) ?
            JsonConvert.DeserializeObject<T>(section.ToString()) : (Value ?? new T());

        applyChanges(sectionObject);

        jObject[_section] = JObject.Parse(JsonConvert.SerializeObject(sectionObject));
        File.SetAttributes(physicalPath, FileAttributes.Normal);
        File.WriteAllText(physicalPath, JsonConvert.SerializeObject(jObject, Formatting.Indented));
    }
}
Matas Vaitkevicius
  • 58,075
  • 31
  • 238
  • 265
  • Could you type up a description of what you "did"? Code-only answers are usually frowned upon because it's very hard to understand if an answer has any value going forward or applies to your situation. A textual description of what kind of changes you performed in order to get it working would go a long way. – Lasse V. Karlsen Feb 18 '20 at 08:50
  • 1
    @LasseV.Karlsen I thought it was text-only answers that were frowned upon, but I am sure if one is determined to frown the reason can be always obtained, lol. Anyway have added brief explanation. – Matas Vaitkevicius Feb 18 '20 at 09:01