How to implement Cocoa copyWithZone on a derived object in MonoMac C #?

I am currently porting a small Winforms-based .NET application to use the built-in Mac interface with MonoMac. The application has a TreeControl with icons and text that does not exist out of the box in Cocoa.

So far, I have ported almost all of the ImageAndTextCell code in the Apple DragNDrop example: https://developer.apple.com/library/mac/#samplecode/DragNDropOutlineView/Listings/ImageAndTextCell_m.html#//apple_ref/doc/uid/DTS4m8831-31 -DontLinkElementID_6 , which is assigned by NSOutlineView as a custom cell.

It seems to work almost perfectly, except that I did not understand how to correctly copyWithZone method. Unfortunately, this means that the internal copies that NSOutlineView makes do not have an image field, and this causes the images to disappear briefly during the expand and collapse operations. Objective-c Code:

 - (id)copyWithZone:(NSZone *)zone { ImageAndTextCell *cell = (ImageAndTextCell *)[super copyWithZone:zone]; // The image ivar will be directly copied; we need to retain or copy it. cell->image = [image retain]; return cell; } 

The first line is what turns me off, since MonoMac does not provide the copyWithZone method, and I do not know what to call it otherwise.

Update

Based on current answers and additional research and testing, I came up with many models for copying an object.

 static List<ImageAndTextCell> _refPool = new List<ImageAndTextCell>(); // Method 1 static IntPtr selRetain = Selector.GetHandle ("retain"); [Export("copyWithZone:")] public virtual NSObject CopyWithZone(IntPtr zone) { ImageAndTextCell cell = new ImageAndTextCell() { Title = Title, Image = Image, }; Messaging.void_objc_msgSend (cell.Handle, selRetain); return cell; } // Method 2 [Export("copyWithZone:")] public virtual NSObject CopyWithZone(IntPtr zone) { ImageAndTextCell cell = new ImageAndTextCell() { Title = Title, Image = Image, }; _refPool.Add(cell); return cell; } [Export("dealloc")] public void Dealloc () { _refPool.Remove(this); this.Dispose(); } // Method 3 static IntPtr selRetain = Selector.GetHandle ("retain"); [Export("copyWithZone:")] public virtual NSObject CopyWithZone(IntPtr zone) { ImageAndTextCell cell = new ImageAndTextCell() { Title = Title, Image = Image, }; _refPool.Add(cell); Messaging.void_objc_msgSend (cell.Handle, selRetain); return cell; } // Method 4 static IntPtr selRetain = Selector.GetHandle ("retain"); static IntPtr selRetainCount = Selector.GetHandle("retainCount"); [Export("copyWithZone:")] public virtual NSObject CopyWithZone (IntPtr zone) { ImageAndTextCell cell = new ImageAndTextCell () { Title = Title, Image = Image, }; _refPool.Add (cell); Messaging.void_objc_msgSend (cell.Handle, selRetain); return cell; } public void PeriodicCleanup () { List<ImageAndTextCell> markedForDelete = new List<ImageAndTextCell> (); foreach (ImageAndTextCell cell in _refPool) { uint count = Messaging.UInt32_objc_msgSend (cell.Handle, selRetainCount); if (count == 1) markedForDelete.Add (cell); } foreach (ImageAndTextCell cell in markedForDelete) { _refPool.Remove (cell); cell.Dispose (); } } // Method 5 static IntPtr selCopyWithZone = Selector.GetHandle("copyWithZone:"); [Export("copyWithZone:")] public virtual NSObject CopyWithZone(IntPtr zone) { IntPtr copyHandle = Messaging.IntPtr_objc_msgSendSuper_IntPtr(SuperHandle, selCopyWithZone, zone); ImageAndTextCell cell = new ImageAndTextCell(copyHandle) { Image = Image, }; _refPool.Add(cell); return cell; } 

Method 1: Increases the number of holds of an unmanaged object The unmanaged object will persist forever (I think dealloc never called), and the managed object will be assembled earlier. It seems that he loses everything around, but works in practice.

Method 2: Save the managed entity reference. The unmanaged object remains at rest, and dealloc is called by the caller in a reasonable amount of time. At this point, the managed entity is freed and deleted. This seems reasonable, but then again, the base dealloc type will not start (I think?)

Method 3: Increases the number of deductions and saves the link. Unmanaged and managed objects go on forever.

Method 4: Extends Method 3 by adding a cleanup function that runs periodically (for example, during the Init of each new ImageAndTextCell object). The cleanup function checks the number of saved saved objects. Holding account 1 means the caller has issued it, so we must do this too. A leak in theory should be eliminated.

Method 5: Attempting to call the copyWithZone method on the base type, and then create a new ImageAndTextView object with the resulting handle. Seems to do the right thing (the underlying data is cloned). Internally, NSObject strikes by saving objects constructed in this way, so we also use the PeriodicCleanup function to free these objects when they are no longer used.

Based on the foregoing, I believe that method 5 is the best approach, since it should be the only one that leads to a truly correct copy of the base type data, but I do not know if this approach is inherently dangerous (I also make some assumptions about the base implementation of NSObject). So far, nothing bad has happened β€œyet,” but if someone could verify my analysis, I would go forward more confidently.

+4
source share
2 answers

So far, I have not found any signs of trouble, so it was convenient for me to adopt "Method 5", which I set out in my update of the question, which I will duplicate here with some additional explanation:

 // An additional constructor public ImageAndTextCell (IntPtr handle) : base(handle) { } // Cocoa Selectors static IntPtr selRetainCount = Selector.GetHandle("retainCount"); static IntPtr selCopyWithZone = Selector.GetHandle("copyWithZone:"); static List<ImageAndTextCell> _refPool = new List<ImageAndTextCell>(); // Helper method to be called at some future point in managed code to release // managed instances that are no longer needed. public void PeriodicCleanup () { List<ImageAndTextCell> markedForDelete = new List<ImageAndTextCell> (); foreach (ImageAndTextCell cell in _refPool) { uint count = Messaging.UInt32_objc_msgSend (cell.Handle, selRetainCount); if (count == 1) markedForDelete.Add (cell); } foreach (ImageAndTextCell cell in markedForDelete) { _refPool.Remove (cell); cell.Dispose (); } } // Overriding the copy method [Export("copyWithZone:")] public virtual NSObject CopyWithZone(IntPtr zone) { IntPtr copyHandle = Messaging.IntPtr_objc_msgSendSuper_IntPtr(SuperHandle, selCopyWithZone, zone); ImageAndTextCell cell = new ImageAndTextCell(copyHandle) { Image = Image, }; _refPool.Add(cell); return cell; } 

By invoking the copyWithZone selector: the selector on the base object (via SuperHandle), the Cocoa base subsystem will clone the unmanaged object and return the handle to it, while the hold counter is already set to 1 (standard obj-c copy of the convention). Then you can build a C # derivative with a cloned object handle, so the cloned instance will become a support object. Then this is a simple cloning issue for any C # managed goodies belonging to a derived type.

As stated in t.speot.is, you must also hold a managed type link. Without reference, the object is a candidate for garbage collection at the end of the method. The unmanaged part of the object is safe to return because it has a positive number of delays from the call to the copy selector. I decided to keep the links in a static list, and then periodically call the cleaning method from other parts of the code that will move through the list, check whether the corresponding unmanaged objects have any other owners, and delete the objects if not. Note that I am checking the count of 1 instead of 0 because our copied object was actually saved twice: once using the copy selector and once using the NSObject constructor. The Monomac Runtime System will take care of disposing of the unmanaged object when the managed side is deleted / collected.

+2
source

This issue is discussed in some part of Error 1086.

Well, this is the problem of ref-counting / ownership:

A new instance of MyObject is created in MyDataSource.GetObjectValue (), then return it to your own code without saving a reference to it. Upon return, you no longer own this object, but the managed garbage collector does not know this.

Just save the objects in a list, for example:

 List<MyObject> list; public MyDataSource () { list = new List<MyObject> (); for (int i = 0; i < 10; i++) { list.Add (new MyObject { Text = "My Row " + i }); } } public override NSObject GetObjectValue (NSTableView tableView, NSTableColumn tableColumn, int row) { return list [row]; } public override int GetRowCount (NSTableView tableView) { return list.Count; } 

However, this does not solve the problem of copyWithZone :. Here, storing cloned objects locally is not an option, it will leak a lot of memory quickly. Instead, you need to call hold on the cloned object. Unfortunately, NSObject.Retain () is internal to MonoMac.dll, but you can just simply do it like this:

 static IntPtr selRetain = Selector.GetHandle ("retain"); [Export("copyWithZone:")] public NSObject CopyWithZone (IntPtr zone) { var cloned = new MyObject { Text = this.Text }; Messaging.void_objc_msgSend (cloned.Handle, selRetain); return cloned; } 

From memory, the code in the last example is not complete, you need to combine the two examples and track the new MyObject in the list (or some other collection).

+2
source

Source: https://habr.com/ru/post/1443918/


All Articles