3
\$\begingroup\$

As always first I have to say that I'm an amateur and not native English speaker, so please have a little patience if I write nonsense ;)

I've worked as an accountant for more than 10 years and since like one year I'm working as a freelancer accountant, so I'm trying to make an accounting app for my personal use (well, lately I wonder this, since the app is getting really big), using c# and WPF.

The thing is that I'm trying to achieve a chrome alike tab-based GUI, with lots of object models that can be duplicated across the different tabs, something that I don't want to happen since some objects can be BIG. So, googling and reading StackOverflow I've come to think in a sort of singleton that store all the object models, which can be asked for any single object. Since I have several assemblies in my solution, I can not access that singleton from any part of the app, so I made a static messenger located in an assembly that is referenced by all the other assemblies (it's called GeneralHelpers), that launch events to which the singleton, located at the only assembly that references all the other assemblies(called AdConta), is subscribed.

The singleton, note that the type switch is not finished, since I'm not sure of some things yet (that's why I'm here ;D I'm not even sure if this is a real singleton or just something else). At the end the type switches should include all the object models used by the app:

namespace AdConta.ModelControl
{
    /// <summary>
    /// Control and store ALL object models. Just for internal use, to add object models use static AppModelControlMessenger.
    /// </summary>
    public class AppModelControl
    {
        public AppModelControl()
        {
            AppModelControlMessenger.ModelAddedEvent += OnModelAddedEvent;
            AppModelControlMessenger.ObjModelAskedEvent += OnObjModelAskedEvent;

            this._Comunidades = new Dictionary<int, Comunidad>();
            this._Personas = new Dictionary<int, Persona>();
            this._Conceptos = new Dictionary<int, Concepto>();
        }

        #region fields
        private Dictionary<int, Comunidad> _Comunidades;
        private Dictionary<int, Persona> _Personas;
        private Dictionary<int, Concepto> _Conceptos;
        #endregion

        #region public methods
        public void UnsubscribeModelControlEvents()
        {
            AppModelControlMessenger.ModelAddedEvent -= OnModelAddedEvent;
            AppModelControlMessenger.ObjModelAskedEvent -= OnObjModelAskedEvent;
        }
        #endregion

        #region events
        /// <summary>
        /// Add object e.Model to the corresponding dictionary, WITHOUT checking if owners exists. The model have to be asked first with 
        /// AppModelControlMessenger.AskForModel
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnModelAddedEvent(object sender, ModelControlEventArgs e)
        {
            //TypeSwitch.Case<>(x=>)
            TypeSwitch.Do(e.ObjectModel,
                TypeSwitch.Case<Comunidad>(x => 
                {
                    Comunidad model = (Comunidad)e.ObjectModel;
                    this._Comunidades.Add(model.Id, model);
                }),
                TypeSwitch.Case<Persona>(x => 
                {
                    Persona model = (Persona)e.ObjectModel;
                    this._Personas.Add(model.Id, model);
                }),
                TypeSwitch.Case<Concepto>(x => 
                {
                    Concepto model = (Concepto)e.ObjectModel;
                    this._Conceptos.Add(model.Id, model);
                })
    #if (MGESTION)
                ,
                TypeSwitch.Case<ComunidadGestion>(x =>
                {
                    ComunidadGestion model = (ComunidadGestion)e.ObjectModel;

                    this._Comunidades[model.IdOwnerComunidad].SetCdadGestion(ref model);
                }),
                TypeSwitch.Case<Finca>(x =>
                {
                    Finca model = (Finca)e.ObjectModel;

                    this._Comunidades[model.IdOwnerComunidad]._Fincas.Add(model.Id, model);
                }),
                /*TypeSwitch.Case<Cuota>(x =>
                {
                    Cuota model = (Cuota)e.Model;

                    this._Comunidades[model.OwnerIdCdad]._Fincas[model.OwnerIdFinca].Cuotas.Add(model.Id, model);
                }),*/
                TypeSwitch.Case<Recibo>(x =>
                {

                })
    #endif
    #if (MCONTABILIDAD)
                ,
                TypeSwitch.Case<ComunidadContabilidad>(x =>
                {
                    ComunidadContabilidad model = (ComunidadContabilidad)e.ObjectModel;

                    this._Comunidades[model.IdOwnerComunidad].SetCdadContabilidad(ref model);
                })
    #endif
            );
        }
        private void OnObjModelAskedEvent(ref object sender, ModelControlEventArgs e)
        {
            object objModel = null;
            int id;

            /*TypeSwitch.Do(e.ObjectModel,
                TypeSwitch.Case<Comunidad>(x => ModelExists = this._Comunidades.ContainsKey(((Comunidad)e.ObjectModel).Id)),
                TypeSwitch.Case<Persona>(x => ModelExists = this._Personas.ContainsKey(((Persona)e.ObjectModel).Id)),
                TypeSwitch.Case<Concepto>(x => ModelExists = this._Conceptos.ContainsKey(((Concepto)e.ObjectModel).Id))
                );*/

            TypeSwitch.Do(e.ObjectModel,
                TypeSwitch.Case<Comunidad>(x =>
                {
                    id = ((Comunidad)e.ObjectModel).Id;
                    if (this._Comunidades.ContainsKey(id)) objModel = this._Comunidades[id];
                    else objModel = null;
                }),
                TypeSwitch.Case<Persona>(x => 
                {
                    id = ((Persona)e.ObjectModel).Id;
                    if (this._Personas.ContainsKey(id)) objModel = this._Personas[id];
                    else objModel = null;
                }),
                TypeSwitch.Case<Concepto>(x =>
                {
                    id = ((Concepto)e.ObjectModel).Id;
                    if (this._Conceptos.ContainsKey(id)) objModel = this._Conceptos[id];
                    else objModel = null;
                })
    #if (MGESTION)
                ,
                TypeSwitch.Case<ComunidadGestion>(x =>
                {
                    id = ((ComunidadGestion)e.ObjectModel).IdOwnerComunidad;
                    if (this._Comunidades.ContainsKey(id) && this._Comunidades[id].CdadGestion.IdOwnerComunidad == id)
                        objModel = this._Comunidades[id].CdadGestion;
                    else objModel = null;
                })
    #endif
    #if (MCONTABILIDAD)
                ,
                TypeSwitch.Case<ComunidadContabilidad>(x =>
                {
                    id = ((ComunidadContabilidad)e.ObjectModel).IdOwnerComunidad;
                    if (this._Comunidades.ContainsKey(id) && this._Comunidades[id].CdadContabilidad.IdOwnerComunidad == id)
                        objModel = this._Comunidades[id].CdadContabilidad;
                    else objModel = null;
                })
    #endif
                );

            AppModelControlMessenger.SetMsgFromAppModelcontrol(ref sender, ref objModel);
        }
        #endregion
    }

}

It have only three dictionaries. To shorten it, all my object models are "contained" as properties by one object of the types Persona, Comunidad or Concepto. Also I've just "categorized" all my object models with interfaces, so I can recognize which type of object contains it and each object have an ID property given by the database which I use as a key, forced by the corresponding interface. As a side note, I "stole" the TypeSwitch class directly from this stackoverflow question.

The messenger:

namespace AdConta.ModelControl
{
    /// <summary>
    /// Static class for adding objectModels so they don't get duplicated.
    /// Use AppModelControlMessenger.AskForObjModel(ref object sender, ref object objModel)
    /// </summary>
    public static class AppModelControlMessenger
    {
        #region fields
        /// <summary>
        /// key = sender, value = objModelAsked
        /// </summary>
        private static Dictionary<object, object> _MsgDict = new Dictionary<object, object>();
        #endregion

        #region events
        public delegate void ModelAddedEventHandler(object sender, ModelControlEventArgs e);
        public static event ModelAddedEventHandler ModelAddedEvent = delegate { };
        /// <summary>
        /// Add object Model to the corresponding dictionary, WITHOUT checking if owners exists. The model have to be asked first with
        /// AppModelControlMessenger.AskForModel
        /// </summary>
        /// <param name="objModel"></param>
        internal static void AddModel(ref object objModel)
        {
            ModelControlEventArgs e = new ModelControlEventArgs(ref objModel);

            ModelAddedEvent(null, e);
        }

        public delegate void ObjModelAskedEventHandler(ref object sender, ModelControlEventArgs e);
        public static event ObjModelAskedEventHandler ObjModelAskedEvent = delegate { };
        /// <summary>
        /// Ask AppModelControl if objModel exists. Return true if objModel exists(and vv) and, if objModel exists, it assign objModel via 
        /// ref parameter to the existing object (objModel = existingObject)
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="objModel"></param>
        /// <returns></returns>
        public static bool AskForObjModel(ref object sender, ref object objModel)
        {
            ModelControlEventArgs e = new ModelControlEventArgs(ref objModel);

            if (!_MsgDict.ContainsKey(sender)) _MsgDict.Add(sender, null);
            ObjModelAskedEvent(ref sender, e);

            if (_MsgDict[sender] == null)
            {
                _MsgDict.Remove(sender);
                AddModel(ref objModel);
                return false;
            }

            objModel = _MsgDict[sender];
            _MsgDict.Remove(sender);
            return true;

            /*bool ret =(_MsgDict[sender] == null ? false : (bool)_MsgDict[sender]);
            _MsgDict.Remove(sender);
            return ret;*/
        }
        #endregion

        #region public methods
        /// <summary>
        /// For internal use only. Don't use it unless you are modifying AppModelControl class.
        /// Set result the of a ModelAskedEvent ing the messages static dictionary.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="objModel"></param>
        public static void SetMsgFromAppModelcontrol(ref object key, ref object objModel)
        {
            if (!_MsgDict.ContainsKey(key)) return;

            _MsgDict[key] = objModel;
        }
        #endregion
    }

    public class ModelControlEventArgs : EventArgs
    {
        public ModelControlEventArgs(ref object objectModel)
        {
            this._ObjectModel = objectModel;
        }

        private object _ObjectModel;
        public object ObjectModel { get { return this._ObjectModel; } }
    }
}

It works basically with the method AskForModel, that method launch the ObjModelAskedEvent. I use that method from the APP so it "asks" AppModelControl if the object model already exists, which makes whatever needed in the private message dictionary (if the object exists, store the object as the value of sender key, if it doesn't exist store null as value). So, I just have to do something like this at any part of the app:

    Person person = new Person();
    AppModelControlMessenger.AskForModel(ref this, ref person);

    //If the object exists, it's assigned to the existing one, if not,     nothing more happens
    //...

I create AppModelControl at the App.xaml.cs, and unsubscribe the events at the App OnExit event:

namespace AdConta
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        public App()
        {
            InitializeComponent();

            FrameworkElement.StyleProperty.OverrideMetadata(typeof(Window), new FrameworkPropertyMetadata
            {
                DefaultValue = FindResource(typeof(Window))
            });

            this._AppModelControl = new ModelControl.AppModelControl();
        }

        private ModelControl.AppModelControl _AppModelControl;

        protected override void OnStartup(StartupEventArgs e)
        {
            FrameworkElement.LanguageProperty.OverrideMetadata(
                typeof(FrameworkElement),
                new FrameworkPropertyMetadata(
                    System.Windows.Markup.XmlLanguage.GetLanguage(
                    System.Globalization.CultureInfo.CurrentCulture.IetfLanguageTag)));
            base.OnStartup(e);
        }

        protected override void OnExit(ExitEventArgs e)
        {
            this._AppModelControl.UnsubscribeModelControlEvents();
            base.OnExit(e);
        }
    }
}

Now, the thing is that I was designing this thing without thinking in removing the objects later, now that I've implemented it, although it seems to work, I've realized that I have to remove all the objects directly from the dictionaries since the dictionaries keep a reference to them, so they don't get garbage collected until the app finish. After searching and reading I found the ConditionalWeakTable class, that seems to let GC to collect the keys stored.

So, the main question is:

Should I use that collection instead of the usual dictionaries, or am I doing something totally wrong with all this and I should rewrite it all? I feel really insecure about this thing although at a first glance, it seems to work. ANY constructive criticism would be really appreciated.

Edit:

It seems that I wasn't clear enough of what I'm trying to achieve.

As I said early, the app have a Chrome alike principal tabcontrol, since I'm using WPF and MVVM, each tab have a view, a viewmodel and a model. Each tab is independent from the others so they don't know what object are using the other ones. Therefore, the user could open twice the same tab with the same objects, or tabs that share only some objects, which would lead to create the same objects twice or more, which I don't want, of course.

And that is it, I just want to not duplicate objects.

The APPModelControl class have a single private instance created at the App class, so there's no way to alter it directly, and "store" a reference of all objects created at different dictionaries. The AppModelControlMessenger class is just a messenger, it's static so it can be accessed from any part of the application. It sends a message to AppModelControl asking if an object exists, so if the object exists, it don't get duplicated, I can just get the existant object through the messenger.

The problem with that system is that it don't remove the objects at the correct time, since a reference is always stored at the dictionaries. Therefore my question about ConditionalWeakTable.

\$\endgroup\$
  • \$\begingroup\$ Have you took into account to use a Dependecy Injection Container like Ninject or Unity? \$\endgroup\$ – JanDotNet Aug 1 '16 at 6:51
  • \$\begingroup\$ I haven't, in fact I knew that it exists but never read about it before. Honestly, after reading that wikipedia link, I fail to see what benefits could it bring to my situation. Maybe it's a lack of knowledge at the subject, could you give me some hint about it? \$\endgroup\$ – Nox Aug 1 '16 at 10:14
  • \$\begingroup\$ Actually, I didn't understand in detail what you are trying to achieve with the class(es) above... but your introduction sounds like you need something that manages the "shared objects" of your application and it's dependencies. DI addresses exactly that problem. However, to become familiare with the concept behind DI takes weeks (or even months)... so my hint is: continue reading and try it out ;) \$\endgroup\$ – JanDotNet Aug 1 '16 at 10:54
  • \$\begingroup\$ Well, I've edited the question, I hope it's clearer now ;) Regarding DI, until some minutes ago I've been searching and reading more about it like you said, and I have to say that i think it's overcomplicate things for my particular case. I don't doubt it would be useful in other cases, but I have nothing that would be changed at runtime, externally or whatever, so i still don't see the need of adding an additional layer like DI. \$\endgroup\$ – Nox Aug 1 '16 at 16:40
3
\$\begingroup\$

OK so your basic issue is that you want to cache model objects that are expensive to create / duplicate. Yes, you could use WeakReferences, but you're probably better off using something like MemoryCache that will allow you better control of when they get destroyed.

I'm not sure I see the need for your static "messenger" class, and I personally really dislike static / singletons because they tend to couple stuff unnecessarily and cause headaches with unit testing. All the type switching is not great. I tend to classify type checking as smelly code

Your basic requirement is that for a given model object type, you want a single place to fetch it from so you can cache instances. This is pretty much a simple Repository.

You could create a generic Repository<T> class, and create an instance for each type you want to cache. If a VM requires access to a type, then the appropriate repository is supplied to the VM constructor. No singletons, no statics, no switching on type. Just a simple class that is passed around.

\$\endgroup\$
  • \$\begingroup\$ I don't understand the repository idea. As far as I know (correct me if I'm wrong) a repository is just a wrapper with CRUD methods, so you can make things like person = PersonRepository.Get(PersonId); without worrying about how database work and what not. How would that avoid duplicated objects? If one tab's viewmodel use that for a particular Person object and two minutes later another tab use another repository for the same Person object, the result will be two exactly equal objects each one in a different viewmodel. Or are you refering to one global repository class for each type? \$\endgroup\$ – Nox Aug 2 '16 at 16:09
  • \$\begingroup\$ Yes, one global repository for each type. In your case your repository wouldn't be hiding database access, but rather it would hide the creation / caching from consumers. The consuming VM's just call Get() and the repository either creates one, or doles out an existing one. \$\endgroup\$ – GazTheDestroyer Aug 2 '16 at 16:18
  • \$\begingroup\$ Oh... so I can have an accesible generic repository interface in my models project and then individual repository classes in each project where each object is defined, so I can access each of them without the static messenger or the singleton. That's perfect, I'll do that. Thanks. \$\endgroup\$ – Nox Aug 2 '16 at 17:51
  • \$\begingroup\$ You may not even need to create repo classes in each project. Just have your generic repo take a factory delegate in it's constructor which it can call to create an object. You can provide the appropriate function at the same time you create the individual repos. \$\endgroup\$ – GazTheDestroyer Aug 3 '16 at 9:07
  • \$\begingroup\$ It's ok now, with all this thing I realized a fail in the projects design so I just reordered it all. Now I don't even need a repository class per project ;), just one generic class and maybe two or three specific class for a few special objects. \$\endgroup\$ – Nox Aug 3 '16 at 10:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.