dimanche 15 mars 2020

automatically detect settings changes using reflection

Hello there stack overflow community, am new and English is not my native language so please be patient with me.

the problem:

i started developing a wpf application using mvvm pattern and faced a problem with having many settings (30+ setting rows some are user defined types that include lists) and needed to implement a way to detect settings changes, so using the "Settings.Default.ProperyChanged" event wasn't enough i needed to detect every value change in all objects that has a ProperyChanged/CollectionChanged event.

my solution:

a helper class that uses reflection to map all settings properties and hook every ProperyChanged/CollectionChanged event to 2 event handlers.

code below is only part of the class code (all explained in comments),please keep in mind that am still learning c# so it might be messy and buggy:

              struct ObservableCollectionInfo
             {
              public object obj { get; set; }
              public PropertyInfo PropertyInfo { get; set; }
              public EventInfo EventInfo { get; set; }
             }

      //mapped lists info 
      private Dictionary<string, ObservableCollectionInfo> ObservableCollectionList;
      //determines if Settings are Changed
      public bool SettingsChanged { get; set; }
      //global count var used in methods below
      int count = 0;

      //Starts mapping settings properties, its called in App class (OnLoadCompleted override)
      public void MappSettingsObjects()
      {
        HookObjectEvents(Settings.Default);
      }

      //object mapping method
      private void HookObjectEvents(object obj)
      {
       count = 0;
       //get object  Properties
       var Properties = obj.GetType().GetProperties();
       //get object  Events
       var Events = obj.GetType().GetEvents();
       //Hook object  Events
       HookEvents(Events, obj);
       foreach (var Property in Properties)
       {
        count++;
        try
        {
         //get Property  Events
         Events = Property.PropertyType.GetEvents();
         //add non observable lists (No NotifyCollectionChanged Event) to Dictionary
         if (Property.GetValue(obj) is IList && Events.Count() == 0)
         {
          var observableCollectionInfo = new ObservableCollectionInfo() { EventInfo = null, obj = obj, PropertyInfo = Property };
          //remove old value if exist
          if (ObservableCollectionList.ContainsKey(Property.Name))
          {
           ObservableCollectionList.Remove(Property.Name);
          }
          //add the new value
          ObservableCollectionList.Add(Property.Name, observableCollectionInfo);
          //maping list items too 
          HookAllListItems(observableCollectionInfo);
         }
         //add Event Handlers
         HookEvents(Events, obj, Property);
        }
        catch (Exception) { }
       }
      }

      private void HookAllListItems(ObservableCollectionInfo info)
      {
       try
       {
        if (info.obj != null)
        {
         var collectionList = info.PropertyInfo.GetValue(info.obj);
         if (collectionList is IList)
         {
          foreach (var item in (collectionList as IList))
          {
           HookObjectEvents(item);
          }
         }
        }
       }
       catch (Exception) { }
      }


      private void HookEvents(EventInfo[] Events, object obj, PropertyInfo Property = null)
      {
       foreach (var Event in Events)
       {
        try
        {
         //comparing events names
         if (Event.EventHandlerType.Name.Equals(nameof(PropertyChangedEventHandler)))
         {
          //add handler
          Event.AddEventHandler(Property == null ? obj : Property.GetValue(obj), new PropertyChangedEventHandler(Default_PropertyChanged));
         }
         else if (Event.EventHandlerType.Name.Equals(nameof(NotifyCollectionChangedEventHandler)))
         {
          var observableCollectionInfo = new ObservableCollectionInfo() { EventInfo = Event, obj = obj, PropertyInfo = Property };
          //add observable list to Dictionary  
          if (ObservableCollectionList.ContainsKey(Property.Name))
          {
           ObservableCollectionList.Remove(Property.Name);
          }
          ObservableCollectionList.Add(Property.Name, observableCollectionInfo);
          HookAllListItems(observableCollectionInfo);
          Event.AddEventHandler(Property == null ? obj : Property.GetValue(obj), new NotifyCollectionChangedEventHandler(Default_CollectionChanged));
         }
         //this condition is to avoid stackoverflow exception (the first Property is the object itself witch causes infinity loop, i dont know why yet)
         if (count > 1)
          //mapping the Property itself
          HookObjectEvents(Property == null ? obj : Property.GetValue(obj));
        }
        catch (TargetParameterCountException)
        {
         //i don't know what causes this exception yet 
        } 
       }
      }

      //Collection Change event handler
      private void Default_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
      {
       //hook new items
       if ((e.Action == NotifyCollectionChangedAction.Add || e.Action == NotifyCollectionChangedAction.Replace) && e.NewItems != null)
        foreach (var item in e.NewItems)
        {
         HookObjectEvents(item);
        }
       //settings changed
       SettingsChanged = true;
      }

      //Property Change event handler
      private void Default_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
      {
       //rehook new lists and its items/events
       if (ObservableCollectionList.ContainsKey(e.PropertyName))
       {
        try
        {
         var info = ObservableCollectionList.FirstOrDefault(li => li.Key == e.PropertyName);
         if (info.Key != null)
         {
          //check if its an observable list or not
          if (info.Value.EventInfo != null)
           //add handler to list changes
           info.Value.EventInfo.AddEventHandler(info.Value.PropertyInfo.GetValue(info.Value.obj), new NotifyCollectionChangedEventHandler(Default_CollectionChanged));
          //hook list items
          HookAllListItems(info.Value);
         }
        }
        catch (Exception) { }
       }
       //settings changed
       SettingsChanged = true;
      }

The code above works fine, all changes are detected even within list items,new items here are some screenshots.

Default Settings:

[https://i.stack.imgur.com/eQPbb.png]

on property changes:

[https://i.stack.imgur.com/u2oK3.png]

on Collection Changes:

[https://i.stack.imgur.com/1Rln3.png]

problems :

  • when using the application for long time while continuously changing the settings makes some changes undetected with no exceptions or warnings what causes this bug ?.

  • with 100+ settings attributes, it starts to slow startup time up to 2-5 seconds, how to improve the code ?.

  • my tutor told me that using reflection is a bad design, so am asking for other mvvm friendly ways if possible (no click/check event handlers in code behind) to detect settings changes i found nothing online.

thanks to anyone that help.





Aucun commentaire:

Enregistrer un commentaire