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
.