Phil Figgins
Phil Figgins

Reputation: 826

Re-instantiate a singleton with Prism in Xamarin Forms

How can I dispose and re-instantiate a singleton with Prism/DryIoC in Xamarin Forms?

I'm working with Azure Mobile Apps for offline data. Occasionally, I need to delete the local sqlite database and re-initialize it. Unfortunately the MobileServiceClient occasionally holds the db connection open and there's no method exposed to close it. The suggested solution (https://github.com/Azure/azure-mobile-apps-net-client/issues/379) is to dispose of MobileServiceClient. Only problem is that is registered with DryIoC as a singleton.

I'm not overly familiar with DryIoC, or Prism and Forms for that matter... But for the life of me, I can't see a way to do this.

I did cook up a pretty elaborate scheme that almost worked.

In my ViewModel method, when I needed the db freed up, I fired off an event -

_eventAggregator.GetEvent<RegisterDatabaseEvent>().Publish(false);

Then in App.xaml.cs, I wired up a listener and a handler like so -

_eventAggregator.GetEvent<RegisterDatabaseEvent>().Subscribe(OnRegisterDatabaseEventPublished);
private void OnRegisterDatabaseEventPublished()
{
    Container.GetContainer().Unregister<IAppMobileClient>();
    Container.GetContainer().Unregister<IMobileServiceClient>();
    Container.GetContainer().Register<IMobileServiceClient, AppMobileClient>(new SingletonReuse());
    Container.GetContainer().Register<IAppMobileClient, AppMobileClient>(new SingletonReuse());

    _eventAggregator.GetEvent<RegisterDatabaseCompletedEvent>().Publish(register);
}

Lastly, back in the ViewModel constructor, I had a final listener that handled the event coming back from App.xaml and finished processing.

_eventAggregator.GetEvent<RegisterDatabaseCompletedEvent>().Subscribe(OnRegisterDatabaseCompletedEventPublished);

So the amazing thing is that this worked. The database was able to be deleted and all was good. But then I navigated to a different page and BOOM. DryIoC said it couldn't wire up the ViewModel for that page. I assume the unregister/register jacked up DryIoC for all injection... So how can I accomplish what needs to be done?

FINAL SOLUTION

Thanks so much to dadhi for taking the time to help. You are certainly a class act and I'm now considering using DryIoC elsewhere.

For anyone who stumbles on this, I'm posting the final solution below. I'll be as verbose as I can to avoid any confusion.

First, in my App.xaml.cs, I added a method for registering my database.

public void RegisterDatabase(IContainer container)
{
    container.RegisterMany<AppMobileClient>(Reuse.Singleton,
        setup: Setup.With(asResolutionCall: true),
        ifAlreadyRegistered: IfAlreadyRegistered.Replace,
        serviceTypeCondition: type =>
            type == typeof(IMobileServiceClient) || type == typeof(IAppMobileClient));
}

I simply add a call to that method in RegisterTypes in place of registering the types in there directly.

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    containerRegistry.GetContainer().Rules.WithoutEagerCachingSingletonForFasterAccess();
...
    RegisterDatabase(containerRegistry.GetContainer());
...
}

Note also the added rule for eager caching, per dadhi.

Later on when I need to release the database in the ViewModel... I kick things off by resetting my local db variable and sending an event to App.xaml.cs

_client = null;
_eventAggregator.GetEvent<RegisterDatabaseEvent>().Publish(true);

In App.xaml.cs, I have subscribed to that event and tied it to the following method.

private void OnRegisterDatabaseEventPublished()
    {
        RegisterDatabase(Container.GetContainer());

        _eventAggregator.GetEvent<RegisterDatabaseCompletedEvent>().Publish(register);
    }

Here I just call RegisterMany again, exactly the same as I do when the app starts up. No need to unregister anything. With the setup and ifAlreadyRegistered arguments (thanks, dadhi!), DryIoC allows the object to be replaced. Then I raise an event back to the VM letting it know the database has been released.

Finally, back in the ViewModel, I'm listening for the completed event. The handler for that event updates the local copy of the object like so.

_client = ((PrismApplication)App.Current).Container.Resolve<IAppMobileClient>();

And then I can work with the new object, as needed. This is key. Without setting _client to null above and resolving it again here, I actually ended up with 2 copies of the object and calls to methods were being hit 2x.

Hope that helps someone else looking to release their Azure Mobile Apps database!

Upvotes: 3

Views: 1330

Answers (1)

dadhi
dadhi

Reputation: 4950

I am not sure how exactly XF handles these things.

But in DryIoc in order for service to be fully deleted or replaced it need to be registered with setup: Setup.With(asResolutionCall: true). Read here for more details: https://bitbucket.org/dadhi/dryioc/wiki/UnregisterAndResolutionCache#markdown-header-unregister-and-resolution-cache

Update

Here are two options and considerations that work in pure DryIoc and may not work XF. But it probably may help with solution.

    public class Foo
    {
        public IBar Bar { get; private set; }
        public Foo(IBar bar) { Bar = bar; }
    }

    public interface IBar {}
    public class Bar : IBar {}
    public class Bar2 : IBar { }

    [Test]
    public void Replace_singleton_dependency_with_asResolutionCall()
    {
        var c = new Container(rules => rules.WithoutEagerCachingSingletonForFasterAccess());

        c.Register<Foo>();
        //c.Register<Foo>(Reuse.Singleton); // !!! If the consumer of replaced dependency is singleton, it won't work
                                            // cause the consumer singleton should be replaced too

        c.Register<IBar, Bar>(Reuse.Singleton,
            setup: Setup.With(asResolutionCall: true));        // required

        var foo = c.Resolve<Foo>();
        Assert.IsInstanceOf<Bar>(foo.Bar);

        c.Register<IBar, Bar2>(Reuse.Singleton,
            setup: Setup.With(asResolutionCall: true),         // required
            ifAlreadyRegistered: IfAlreadyRegistered.Replace); // required

        var foo2 = c.Resolve<Foo>();
        Assert.IsInstanceOf<Bar2>(foo2.Bar);
    }

    [Test]
    public void Replace_singleton_dependency_with_UseInstance()
    {
        var c = new Container();

        c.Register<Foo>();
        //c.Register<Foo>(Reuse.Singleton); // !!! If the consumer of replaced dependency is singleton, it won't work
                                            // cause the consumer singleton should be replaced too
        c.UseInstance<IBar>(new Bar());
        var foo = c.Resolve<Foo>();
        Assert.IsInstanceOf<Bar>(foo.Bar);

        c.UseInstance<IBar>(new Bar2());
        var foo2 = c.Resolve<Foo>();
        Assert.IsInstanceOf<Bar2>(foo2.Bar);
    }

Upvotes: 1

Related Questions