Getting Mono.Cecil to rewrite PDB files to enable debugging
January 19, 2011
I was asked how to get Mono.Cecil to rewrite PDB files so that VS can step through source when debugging. Here’s an excerpt of the code I’m using to do this. It works fine for me!
var assemblyResolver = new DefaultAssemblyResolver();
var assemblyLocation = Path.GetDirectoryName(AssemblyPath);
assemblyResolver.AddSearchDirectory(assemblyLocation);
if (!string.IsNullOrEmpty(HintPath))
{
assemblyResolver.AddSearchDirectory(HintPath);
}
var silverlightAssemblyPath = Environment.ExpandEnvironmentVariables(@”%ProgramFiles%\Reference Assemblies\Microsoft\Framework\Silverlight\v4.0\”);
assemblyResolver.AddSearchDirectory(silverlightAssemblyPath);
var readerParameters = new ReaderParameters { AssemblyResolver = assemblyResolver };
var writerParameters = new WriterParameters();
var pdbName = Path.ChangeExtension(AssemblyPath, “pdb”);
if (File.Exists(pdbName))
{
var symbolReaderProvider = new PdbReaderProvider();
readerParameters.SymbolReaderProvider = symbolReaderProvider;
readerParameters.ReadSymbols = true;
writerParameters.WriteSymbols = true;
}
var assemblyDefinition = AssemblyDefinition.ReadAssembly(AssemblyPath, readerParameters);
var weaver = new NotifyPropertyChangedWeaver();
weaver.Weave(assemblyDefinition);
assemblyDefinition.Write(AssemblyPath, writerParameters);
Debugging Pain of INotifyPropertyChanged with Mono Cecil
December 8, 2010
So I officially hate my life! I just lost a day of my life trying to get NotifyPropertyChanged weaving working with Mono.Cecil. It worked brilliantly in .Net, but blew up in Silverlight with a VerificationException (“Operation could destabilize the runtime”) as soon as I tried to invoke my rewritten Property Setters.
I literally combed through every IL instruction in both Reflector and ILDASM and verified everything was as it should be. In the end the issue was a peculiarity with the CoreCLR as oposed to the full blown CLR. The CoreCLR requires all locals to be flagged as init even if play by the rules and assign to the local before reading from it.
Here’s the offending IL:
.method public hidebysig specialname instance void set_Property(string 'value') cil managed { .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() .maxstack 2 .locals init ( [0] string str, [1] bool flag) L_0000: nop L_0001: ldarg.0 L_0002: call instance string SynerG.Weaving.Tests.DecoratedClass::get_Property() L_0007: stloc.0 L_0008: ldarg.0 L_0009: ldarg.1 L_000a: stfld string SynerG.Weaving.Tests.DecoratedClass::<Property>k__BackingField L_000f: ldarg.1 L_0010: ldloc.0 L_0011: call bool [mscorlib]System.Object::Equals(object, object) L_0016: stloc.1 L_0017: ldloc.1 L_0018: brtrue.s L_0028 L_001a: nop L_001b: ldarg.0 L_001c: ldstr "Property" L_0021: callvirt instance void [SynerG.Silverlight]SynerG.Notification.NotifyingBase::OnPropertyChanged(string) L_0026: nop L_0027: nop L_0028: ret } |
You can fix this with a cheeky:
methodBody.InitLocals = true;
Phew. That’s a day of my life that I’m never going to get back again!
INotifyPropertyChanged Weaving
December 2, 2010
A common gripe with WPF and Silverlight development is having to implement INotifyPropertyChanged in your Models and ViewModels. You then have to remember (most of the time) to fire the PropertyChanged event whenever you change a property.
On the project I’m working on we’ve been using PostSharp for a while to automatically weave in the code to do this for us. The other day I set about converting our app to .Net 4 and Silverlight 4. Sadly, the version of PostSharp we were using (1.5) doesn’t play nicely with .Net 4. Fine I thought, I’ll simple update to version 2.0. I then found out that the functionality we need is only available in the licensed version. Although PostSharp is a great product, we only use it for this exact problem and it doesn’t justify forking out for a license for all the devs on my team.
So I decided to set out weaving in the IL myself. I tried various different methods including Microsoft’s CCI and CciSharp. I found these to be very powerful, but fiendishly complicated. Then I tried out Mono.Cecil. This is a fantastic bit of software and is very easy to use.
I stumbled upon a great blog post by Justin Angel. It describes exactly how you can achieve this using Mono.Cecil. The only problem is that if you rewrite a silverlight application assembly in the AfterBuild Target as Justin describes, the XAP file generated contains the DLL before the rewrite and therefore doesn’t work.
I managed to get around this problem by hacking my .csproj file thusly:
<UsingTask TaskName=”CecilPropertyChanged.MSBuild.WeaveNotifyPropertyChangedTask” AssemblyFile=”$(SolutionDir)Lib\CecilPropertyChanged.MSBuild.dll” />
<Target Name=”WeaveNotifyPropertyChanged”>
<CecilPropertyChanged.MSBuild.WeaveNotifyPropertyChangedTask AssemblyPath=”$(IntermediateOutputPath)$(AssemblyName).dll” />
<Copy SourceFiles=”$(IntermediateOutputPath)$(AssemblyName).dll” DestinationFolder=”$(OutputPath)” />
</Target>
<PropertyGroup>
<XapPackagerDependsOn>
_CreateSLProperties;
MarkupCompilePass1;
ValidateXaml;
WeaveNotifyPropertyChanged;
FilesToXap;
CreateSilverlightAppManifest;
</XapPackagerDependsOn>
</PropertyGroup>
Works like a charm!
Regionerate Rocks!
September 15, 2009
I’ve just stumbled across Regionerate. It’s a fantastic little tool that automatically applies regions to C# code. For a code nazi like me this is invaluable and will likely spare me from RSI and lifetime of misery!
I also wrote a custom configuration file that matches my particular form of OCD (obsessive code disorder):
<?xml version="1.0" encoding="utf-16"?> <CodeLayout xmlns="http://regionerate.net/schemas/0.7.0.0/CodeLayout.xsd"> <ForEach Type="Class"> <CreateRegion Title="Public Static Fields" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutFields> <Where> <Static Equals="true"/> <Access Equals="Public" /> </Where> <OrderBy> <Name /> </OrderBy> </PutFields> </CreateRegion> <CreateRegion Title="Internal Static Fields" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutFields> <Where> <Static Equals="true"/> <Access Equals="Internal" /> </Where> <OrderBy> <Name /> </OrderBy> </PutFields> </CreateRegion> <CreateRegion Title="Protected Static Fields" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutFields> <Where> <Static Equals="true"/> <Access Equals="Protected" /> </Where> <OrderBy> <Name /> </OrderBy> </PutFields> </CreateRegion> <CreateRegion Title="Private Static Fields" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutFields> <Where> <Static Equals="true"/> <Access Equals="Private" /> </Where> <OrderBy> <Name /> </OrderBy> </PutFields> </CreateRegion> <CreateRegion Title="Public Fields" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutFields> <Where> <Static Equals="false"/> <Access Equals="Public" /> </Where> <OrderBy> <Name /> </OrderBy> </PutFields> </CreateRegion> <CreateRegion Title="Internal Fields" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutFields> <Where> <Static Equals="false"/> <Access Equals="Internal" /> </Where> <OrderBy> <Name /> </OrderBy> </PutFields> </CreateRegion> <CreateRegion Title="Protected Fields" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutFields> <Where> <Static Equals="false"/> <Access Equals="Protected" /> </Where> <OrderBy> <Name /> </OrderBy> </PutFields> </CreateRegion> <CreateRegion Title="Private Fields" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutFields> <Where> <Static Equals="false"/> <Access Equals="Private" /> </Where> <OrderBy> <Name /> </OrderBy> </PutFields> </CreateRegion> <CreateRegion Title="Static Ctor" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutMethods> <Where> <IsConstructor Equals="true" /> <Static Equals="true"/> </Where> <OrderBy> <ParametersCount /> </OrderBy> </PutMethods> </CreateRegion> <CreateRegion Title="Ctor" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutMethods> <Where> <IsConstructor Equals="true" /> <Static Equals="false"/> </Where> <OrderBy> <ParametersCount /> </OrderBy> </PutMethods> </CreateRegion> <CreateRegion Title="Public Static Methods" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutMethods> <Where> <IsConstructor Equals="false" /> <Static Equals="true" /> <Access Equals="Public"/> </Where> <OrderBy> <ParametersCount /> </OrderBy> </PutMethods> </CreateRegion> <CreateRegion Title="Internal Static Methods" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutMethods> <Where> <IsConstructor Equals="false" /> <Static Equals="true" /> <Access Equals="Internal"/> </Where> <OrderBy> <ParametersCount /> </OrderBy> </PutMethods> </CreateRegion> <CreateRegion Title="Protected Static Methods" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutMethods> <Where> <IsConstructor Equals="false" /> <Static Equals="true" /> <Access Equals="Protected"/> </Where> <OrderBy> <ParametersCount /> </OrderBy> </PutMethods> </CreateRegion> <CreateRegion Title="Private Static Methods" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutMethods> <Where> <IsConstructor Equals="false" /> <Static Equals="true" /> <Access Equals="Private"/> </Where> <OrderBy> <ParametersCount /> </OrderBy> </PutMethods> </CreateRegion> <CreateRegion Title="Public Methods" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutMethods> <Where> <IsConstructor Equals="false" /> <Static Equals="false" /> <Access Equals="Public"/> </Where> <OrderBy> <ParametersCount /> </OrderBy> </PutMethods> </CreateRegion> <CreateRegion Title="Internal Methods" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutMethods> <Where> <IsConstructor Equals="false" /> <Static Equals="false" /> <Access Equals="Internal"/> </Where> <OrderBy> <ParametersCount /> </OrderBy> </PutMethods> </CreateRegion> <CreateRegion Title="Protected Methods" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutMethods> <Where> <IsConstructor Equals="false" /> <Static Equals="false" /> <Access Equals="Protected"/> </Where> <OrderBy> <ParametersCount /> </OrderBy> </PutMethods> </CreateRegion> <CreateRegion Title="Private Methods" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutMethods> <Where> <IsConstructor Equals="false" /> <Static Equals="false" /> <Access Equals="Private"/> </Where> <OrderBy> <ParametersCount /> </OrderBy> </PutMethods> </CreateRegion> <CreateRegion Title="Events" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutEvents> <OrderBy> <Name /> </OrderBy> </PutEvents> </CreateRegion> <CreateRegion Title="Public Static Properties" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutProperties> <Where> <Static Equals="true" /> <Access Equals="Public"/> </Where> <OrderBy> <Name /> </OrderBy> </PutProperties> </CreateRegion> <CreateRegion Title="Internal Static Properties" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutProperties> <Where> <Static Equals="true" /> <Access Equals="Internal"/> </Where> <OrderBy> <Name /> </OrderBy> </PutProperties> </CreateRegion> <CreateRegion Title="Protected Static Properties" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutProperties> <Where> <Static Equals="true" /> <Access Equals="Protected"/> </Where> <OrderBy> <Name /> </OrderBy> </PutProperties> </CreateRegion> <CreateRegion Title="Private Static Properties" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutProperties> <Where> <Static Equals="true" /> <Access Equals="Private"/> </Where> <OrderBy> <Name /> </OrderBy> </PutProperties> </CreateRegion> <CreateRegion Title="Public Properties" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutProperties> <Where> <Static Equals="false" /> <Access Equals="Public"/> </Where> <OrderBy> <Name /> </OrderBy> </PutProperties> </CreateRegion> <CreateRegion Title="Internal Properties" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutProperties> <Where> <Static Equals="false" /> <Access Equals="Internal"/> </Where> <OrderBy> <Name /> </OrderBy> </PutProperties> </CreateRegion> <CreateRegion Title="Protected Properties" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutProperties> <Where> <Static Equals="false" /> <Access Equals="Protected"/> </Where> <OrderBy> <Name /> </OrderBy> </PutProperties> </CreateRegion> <CreateRegion Title="Private Properties" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutProperties> <Where> <Static Equals="false" /> <Access Equals="Private"/> </Where> <OrderBy> <Name /> </OrderBy> </PutProperties> </CreateRegion> <CreateRegion Title="Delegates" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutDelegates> <OrderBy> <Name /> </OrderBy> </PutDelegates> </CreateRegion> <CreateRegion Title="Nested Classes" PadFirstChild="0" PadLastChild="0" ShowCount="false" Style="Visible"> <PutNestedClasses> <OrderBy> <Name /> </OrderBy> </PutNestedClasses> </CreateRegion> </ForEach> <Configuration> <Symbol> <HiddenDragon /> </Symbol> <Rendering ShowCount="false" /> </Configuration> </CodeLayout>
Hot-swappable Themes in Silverlight
September 14, 2009
I’ve been playing around with hot-swapping themes in Silverlight recently. This turns out to be trickier than you might imagine due to a couple of facts:
- Silverlight doesn’t support DynamicResources
- Silverlight doesn’t support implicit styles
And no, I don’t count the ImplicitStyleManager in the SilverlightToolkit as it’s buggy and performs like a dog. At any rate, implicit styles soon become completely unmanageable in an application of any considerable size.
The solution I came up with was to have a ThemingService that automatically applies Styles to FrameworkElements. The FrameworkElements register themselves with the ThemingService by setting an attached StyleKey dependency property. The ThemingService then maintains a WeakReference to the FrameworkElement and simply walks down the list of registered elements whenever a new theme is applied.
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Windows; using System.Windows.Markup; using System.Windows.Threading; using Microsoft.Practices.Composite.Events; using Microsoft.Practices.Composite.Logging; using Microsoft.Practices.Composite.Presentation.Events; using ThemingPlay.Core.Configuration; namespace ThemingPlay.Core.Theming { public class ThemingService : IThemingService { #region Private Fields private readonly IConfigurationService _configurationService; private readonly ConfigurationLoadedEvent _configurationLoadedEvent; private readonly ThemesLoadedEvent _themesLoadedEvent; private readonly ThemeChangedEvent _themeChangedEvent; private readonly ILoggerFacade _logger; private readonly Dispatcher _dispatcher; private readonly IList<WeakReference> _elementReferences; private readonly IList<Theme> _themes; private Theme _currentTheme; #endregion #region Ctor public ThemingService(IConfigurationService configurationService, IEventAggregator eventAggregator, Dispatcher dispatcher, ILoggerFacade logger) { _configurationService = configurationService; _configurationLoadedEvent = eventAggregator.GetEvent<ConfigurationLoadedEvent>(); _configurationLoadedEvent.Subscribe(OnConfigurationLoaded, ThreadOption.UIThread); _themesLoadedEvent = eventAggregator.GetEvent<ThemesLoadedEvent>(); _themeChangedEvent = eventAggregator.GetEvent<ThemeChangedEvent>(); _dispatcher = dispatcher; _logger = logger; _elementReferences = new List<WeakReference>(); _themes = new List<Theme>(); } #endregion #region Dependency Properties public static readonly DependencyProperty StyleKeyProperty = DependencyProperty.RegisterAttached( "StyleKey", typeof (string), typeof (ThemingService), new PropertyMetadata(OnStyleKeyChanged)); public static void SetStyleKey(FrameworkElement element, string styleKey) { element.SetValue(StyleKeyProperty, styleKey); } public static string GetStyleKey(FrameworkElement element) { return element.GetValue(StyleKeyProperty) as string; } private static void OnStyleKeyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var styledElement = (FrameworkElement) d; var themingService = SingletonContainer.Instance.Resolve<IThemingService>(); themingService.RegisterThemedElement(styledElement); } #endregion #region Implementation of IThemingService public IList<Theme> Themes { get { return _themes; } } public Theme CurrentTheme { get { return _currentTheme; } set { _currentTheme = value; SwitchToTheme(_currentTheme); _themeChangedEvent.Publish(_currentTheme); } } public void RegisterThemedElement(FrameworkElement element) { _elementReferences.Add(new WeakReference(element)); if (_currentTheme != null && _currentTheme.ResourceDictionary != null) { ApplyStyleToElement(_currentTheme.ResourceDictionary, element); } } #endregion #region Composite Event Handlers public void OnConfigurationLoaded(object nothing) { LoadThemesFromConfiguration(); } #endregion #region Private Methods private void LoadThemesFromConfiguration() { var themesConfig = _configurationService[THEMES_CONFIGURATION_SECTION] as ThemesConfigurationSection; if (themesConfig != null) { foreach (var themeConfig in themesConfig.Themes) { var theme = CreateTheme(themeConfig); _themes.Add(theme); } var defaultTheme = (from t in _themes where t.Name == themesConfig.DefaultTheme select t).SingleOrDefault(); if (defaultTheme != null) { this.CurrentTheme = defaultTheme; } _themesLoadedEvent.Publish(_themes); } } private static Theme CreateTheme(ThemeConfigurationElement themeConfig) { var theme = new Theme { Name = themeConfig.Name, Description = themeConfig.Description, DictionaryUri = new Uri(themeConfig.DictionaryUri, UriKind.Relative), PackageUri = UriHelper.AbsoluteUriFromRelativePath(themeConfig.PackageUri) }; return theme; } private void LoadCurrentThemeAsync(Theme theme) { var request = WebRequest.Create(theme.PackageUri); request.BeginGetResponse(result => _dispatcher.BeginInvoke(()=> { try { var response = request.EndGetResponse(result); using (var stream = response.GetResponseStream()) { var assemblyParts = XapHelper.GetAssemblyParts(stream); if (assemblyParts.Count == 1) { var assemblyPart = assemblyParts[0]; if (XapHelper.LoadAssemblyFromStream(stream, assemblyPart) != null) { var sri = Application.GetResourceStream(theme.DictionaryUri); using (var xamlStream = sri.Stream) { using (var reader = new StreamReader(xamlStream)) { var xaml = reader.ReadToEnd(); var resourceDictionary = XamlReader.Load(xaml) as ResourceDictionary; if (resourceDictionary != null) { theme.ResourceDictionary = resourceDictionary; ApplyTheme(theme); } } } } } } } catch (Exception ex) { _logger.Log(ex.Message, Category.Exception, Priority.High); } }), null); } private void SwitchToTheme(Theme theme) { if (theme.ResourceDictionary != null) { ApplyTheme(theme); } else { LoadCurrentThemeAsync(theme); } } private void ApplyTheme(Theme theme) { ICollection<WeakReference> deadElements = null; foreach (var elementReference in _elementReferences) { if (elementReference.IsAlive) { var styledElement = elementReference.Target as FrameworkElement; if (styledElement != null) { ApplyStyleToElement(theme.ResourceDictionary, styledElement); } } else { if (deadElements == null) { deadElements = new List<WeakReference>(); } deadElements.Add(elementReference); } } if (deadElements != null) { foreach (var deadElement in deadElements) { _elementReferences.Remove(deadElement); } } } private void ApplyStyleToElement(ResourceDictionary resourceDictionary, FrameworkElement element) { var styleKey = GetStyleKey(element); if (!string.IsNullOrEmpty(styleKey)) { if (resourceDictionary.Contains(styleKey)) { var style = resourceDictionary[styleKey] as Style; if (style != null) { try { element.Style = style; } catch (Exception ex) { _logger.Log(ex.Message, Category.Exception, Priority.High); } } } } } #endregion #region Constants private static readonly string THEMES_CONFIGURATION_SECTION = "themes"; #endregion } }
There’s loads of other code in here to do with Prism and downloading the Themes dynamically, but you get the point…
Silverlight Web Request on Application Exit
August 17, 2009
I was just looking into persisting the state of a Silverlight application on exit. The scenario is that the application has a complex layout that we normally persist to a database via a RESTful service call. The problem is what if the user forgets to explicitly save the layout before shutting the browser down. So I looked into calling the service in the Application_Exit method in the App class. Sadly this does not work – it appears that the request is queued on the Dispatcher thread after the shutdown process and so never executes. Guess I’ll have to save the state to isolated storage and put some hacky code in to determine if there is a saved state in ISO before loading the layout from the DB. Bit of a bummer if the user uses more than one machine though…
Playing Around with System.Reactive Again
August 17, 2009
Further to my previous post, Erik Meijer commented that an overload to the FromEvent method is available to convert from generic event handlers to the specific ones used by much of the BCL. So the code from the previous post would look something like this now:
#region Ctor public MainPage() { InitializeComponent(); var mouseEventSource = Observable.FromEvent<MouseEventHandler, MouseEventArgs>( eh => new MouseEventHandler(eh), // Conversion eh => this.MouseMove += eh, // Subscribe eh => this.MouseMove -= eh); // Unsubscribe _mouseMoveSubscription = mouseEventSource.Subscribe(OnMouseMove); } #endregion private void OnMouseMove(Event<MouseEventArgs> e) { var position = e.EventArgs.GetPosition(this); Debug.WriteLine("Mouse moved. Position: {0}.", position); }
This solves one of my major gripes. Exciting times!
A base class for Domain Models
August 14, 2009
I just wrote a base class that I’m going to derive all of my notifying Domain Models from. It solves the horror of passing around the property name of a changing property in the INotifyPropertyChanged implementation. This has the enormous benefit of being able to check your Property Names at compile-time and also allows you to keep everything in sync when using tools like Resharper (if you don’t use Resharper – then you really should!) to rename things. It uses lambda expressions to specify the Property that’s changing.
public abstract class ModelBase<T> : INotifyPropertyChanged where T : ModelBase<T> { #region Events public event PropertyChangedEventHandler PropertyChanged; #endregion #region Public Methods public static string GetPropertyName<R>(Expression<Func<T, R>> expression) { var memberExpression = expression.Body as MemberExpression; if (memberExpression == null) { throw new ArgumentException("'expression' should be a member expression"); } var propertyName = memberExpression.Member.Name; return propertyName; } #endregion #region Protected Methods protected void OnPropertyChanged(string propertyName) { if (this.PropertyChanged != null) { this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } protected void RaisePropertyChanged<R>(Expression<Func<T, R>> expression) { var propertyName = GetPropertyName(expression); this.OnPropertyChanged(propertyName); } #endregion }
Following on from my woes about ObservableCollection<T>, and my annoyance at having to maintain a parallel collection to ensure all INotifyPropertyChanged and INotifyCollectionChanged subscriptions are removed, I decided to look down the avenue of Weak event subscriptions. To that end I can up with the classes WeakPropertyChangedListner and WeakCollectionChangedListener. The code for these is as follows:
public class WeakPropertyChangedListener { #region Private Fields private INotifyPropertyChanged _source; private WeakReference _listener; #endregion #region Ctor public WeakPropertyChangedListener(INotifyPropertyChanged source, PropertyChangedEventHandler listener) { this._source = source; this._source.PropertyChanged += this.OnPropertyChanged; this._listener = new WeakReference(listener); } #endregion #region Public Methods public static WeakPropertyChangedListener Create(INotifyPropertyChanged source, PropertyChangedEventHandler listener) { return new WeakPropertyChangedListener(source, listener); } public void Disconnect() { if (_source != null) { _source.PropertyChanged -= this.OnPropertyChanged; _source = null; _listener = null; } } #endregion #region Private Methods private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) { if (_listener != null) { var handler = _listener.Target as PropertyChangedEventHandler; if (handler != null) { handler(sender, e); } else { this.Disconnect(); } } } #endregion }
and:
public class WeakCollectionChangedListener { #region Private Fields private INotifyCollectionChanged _source; private WeakReference _handler; #endregion #region Ctor public WeakCollectionChangedListener(INotifyCollectionChanged source, NotifyCollectionChangedEventHandler handler) { this._source = source; this._source.CollectionChanged += this.OnCollectionChanged; this._handler = new WeakReference(handler); } #endregion #region Public Methods public static WeakCollectionChangedListener Create(INotifyCollectionChanged source, NotifyCollectionChangedEventHandler handler) { return new WeakCollectionChangedListener(source, handler); } public void Disconnect() { if (_source != null) { _source.CollectionChanged -= this.OnCollectionChanged; _source = null; _handler = null; } } #endregion #region Private Methods private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (_handler != null) { var handler = _handler.Target as NotifyCollectionChangedEventHandler; if (handler != null) { handler(sender, e); } else { this.Disconnect(); } } } #endregion }
Then you simple subscribe like this:
WeakPropertyChangedListener.Create(person, OnPersonPropertyChanged); WeakCollectionChangedListener.Create(person.Children, OnPersonCollectionChanged);
Safe in the knowledge that your observing the notifications will not cause your domain objects to hang around.