Hi,
I've started looking into the Undo/Redo framework, and have a scenario I currently fail to find a quick solution to in the documentation.
The issue is that my undo transactions will involve objects that need some cleanup before being garbage collected. This may include detaching event handlers and/or deleting some files on disk. If I e.g. delete such objects from my object graph within an undo transaction I obviously can't clean them up immediately, since the objects may be brought back during a redo. What I need is a way to clean up these objects when they fall off the UndoHistory and are gueranteed to be gone forever.
What is the recommended pattern to solve this issue ?
Regards,Leif
There is (at least currently) no notification made to an undounit or otherwise when an item is removed from the history. Typically if one is holding unmanaged resources then one might implement a finalizer (and IDisposable) and handle releasing those unmanaged resources in there. Perhaps the managed object you are referencing (e.g. a FileStream or more accurately the SafeFileHandle of a FileStream) would automatically handle that when it is finalized. Or perhaps you manage some weak dictionary of references to your object and periodically check to see if that weak reference is released. If so then you would do your clean up. Typically I would think that file deletion is not something that is deferred. I mean if the user closes the app you would still need to do that clean up before the app shuts down or worst if the app were to be forceably shut down or crash then those deletions would not have actually occurred.
With regards to event handlers it would depend upon the situation. If the object is in the undo history because it was removed from a collection for example then typically I would think you would want to disconnect that object and unhook any events. Should that object instance be re-added to the collection then you would reconnect it and hook whatever events you needed to. If they are hooking static events and need to stay hooked in then perhaps you would use one of the weak event patterns to ensure the object is not rooted by that static event handler.
thanks for the answer. We really would like a deterministic way to delete our files, and not rely on e.g. finalizers. We also can't wait until the application is finished to do such a cleanup, since the files in question are very big.
Just to describe our scenario a bit. We have an application that is able to process huge amounts of Ground Penetrating Radar (GPR) data. The output of each processing job may result in physical result files with sizes of several hundred megabytes each. We would like to give the user an option to revert individual processing jobs (or batches of such jobs) which makes these jobs end up in the undo list. This may populate the undo list with processing job objects that may be backed by hundreds of megabytes of disk space that the application is otherwise done with unless the user selects to undo the operation(s). To not actually make the user run out of disk space it is vital for us to know when the last reference to these files (in this case the undo history) is gone, so that we can delete them immediately.
I'm sure we can find a workaround for this that may work, but I suspect that this will include code that add unneeded complexity compared to a simple cleanup callback from the Undo framework.
Technically you don't even have to subclass it. All its doing is hooking the CollectionChanged event which could be done externally. With regards to deleting as quickly as possible I wasn't saying not to delete at all. I was just pointing out that you would essentially be leaving some number of items (which in your case are because they are still in the undo history) around and you need to clean those up (i.e. delete those files) when the application closes. The undo manager wouldn't know about the application closing and if it did it wouldn't be trying to clear the history as a result. With regards to the recycle bin I don't see how that gives you nothing since the OS is only going to store as much of the deleted files as it can up to the set limit that was set (by the end user) on their recycle bin size and so that information will be reclaimed when there isn't enough room in the recycle bin or when the user explicitly empties the recycle bin. I merely pointed out option since that is essentially what the os does when you delete a file although I believe they will prompt you if you try to delete a large file to find out if it should put it in the recycle bin and that would avoid the possibility that while the user did delete a file it would still exist on their system if your application crashes or is forcibly closed and you don't get to actually try to clean up (i.e. remove those files associated with items still in the undo history).
Yes, the case when one has set the UndoLimit and a new change is being recorded is the situation applicable for us. Subclassing the UndoManager to do our own constraining might actually be a solution - I'll look into this. Thanks.
When it comes to the file deletions our focus is to free disk space as quickly as possible, since the amount of data in these files are so huge that the user may very well run out of disk space. If we don't implement an undo feature we can delete them at once when we're starting a new processing that replaces the data. Keeping them all until the application finishes (with undo implemented) we will most certainly fill the user's disk with processed data before the session ends. Letting the recycle bin handle the file will therefore give us nothing since it will still occupy huge amounts of disk space.
I'll consider submitting a feature suggestion. It is quite simple: Associate an optional Action with an undo unit - an action that is called when the undo edit is removed from a limited undo history stack.
Perhaps I'm missing something. You're saying that you can't (or don't want to) delete the files while the associated operation is in the undo history but then you also say that you can't wait until the application is finished to delete but whatever is in the undo history will have to then be kept around until the application is closed and deleted then. One alternative might be to delete the file and let the OS recycle bin handle keeping the file. If you go to undo the deletion then you would try to recover that file from the recycle bin. Perhaps using some code discussed in this MS forum post. With regards to adding some functionality you can submit suggestions on our uservoice site with as much detail as possible and they will be considered for inclusion in future versions of the product. It's a bit unclear to me exactly what you want because there are multiple ways that undounits will come off the history. Items could come off when one has set the UndoLimit and a new change is being recorded - it sounds like this is the only situation that concerns you. It also happens when an item is popped off the undo or redo history when it is about to be performed. Items can also fall off the redo history if one has performed one or more undo operations and then records a new change (which clears the redo history). It can also happen programatically when one uses the RemoveAll method or calls ClearHistory.That being said you might be able to just do something like this yourself. Instead of setting the UndoLimit you could leave it unlimited and then implement your own constraining logic. In this way you control when the items are removed and can do whatever processing you want when you remove them from the history. e.g.
public class UndoManagerEx : UndoManager{ private int _preferredHistoryCount = 10; public UndoManagerEx() { ((INotifyCollectionChanged)this.UndoHistory).CollectionChanged += OnUndoHistoryChanged; } private void OnUndoHistoryChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.Action == NotifyCollectionChangedAction.Add) { Dispatcher.CurrentDispatcher.BeginInvoke(new Action(VerifyUndoHistoryLimit), null); } } private void VerifyUndoHistoryLimit() { if (this.UndoHistory.Count > _preferredHistoryCount) { var itemsToRemove = this.UndoHistory.Skip(_preferredHistoryCount).Select((hi) => hi.Unit).ToArray(); this.RemoveAll((u) => itemsToRemove.Contains(u), false); foreach (var item in itemsToRemove) { Debug.WriteLine("Removed:" + item.GetDescription(UndoHistoryItemType.Undo, false)); } } }}