0

I'm trying to develop a pair of C# applications, one iOS, one Mac, that use the same CoreData database. Effectively, the desktop app populates it, and then it's distributed as part of the iOS app.

I can use XCode to generate an .xcdatamodeld file describing what I want the database to look like. I can use momc to compile that into a .momd file. I can include the .mom file from within that into my Mono project, and load it into a NSManagedObjectModel, from which I can access all the properties of the various entities.

What I haven't yet figured out how to do is create an object of a class from the database, rather than accessing the properties of the table. Any suggestions?


To clarify: I want to be able to create a table/class in XCode, call it Person. I give it two fields: Name and Phone. I want to be able to run code similar to this in Mono:

using (var context = new NSManagedObjectContext())
{
  var me = context.Person.GetByID(1);
  me.Name = "Bobson";
  context.Save();
}

Obviously, the specifics of getting it from the database and saving it back will be different, but the gist is there.

Bobson
  • 13,498
  • 5
  • 55
  • 80
  • you may need to clarify your question. It is unclear to me what you are after. – TrustMe Sep 25 '12 at 18:42
  • 1
    @TrustMe - Essentially, what I'm looking for is a way to use XCode's CoreData designer to generate a .mom file and some classes the same way I can use the Entity Framework designer to generate an .edmx file and some classes, or the LINQ-to-SQL designer to generate a .dbml file and some classes. Generating the classes is the part that's escaped me so far, although I'm just about to try your answer. – Bobson Sep 25 '12 at 22:04
  • 1
    Have you considered not using CoreData? Since you are developing in C#, a common approach would be to use one of the many .NET ORMs that have been ported to / work on MonoTouch. I use [Catnap](https://github.com/timscott/catnap). You can create the low-level schema using a SQLite desktop client. I use [Navicat](http://www.navicat.com/en/products/navicat_sqlite/sqlite_detail_mac.html). The benefit of this approach is you can use same code on Android and other platforms if you ever need to. – t9mike Sep 26 '12 at 16:29
  • @t9mike - I considered it, but things like http://stackoverflow.com/a/1045560/298754 and various discussions of the hand-optimizing required for a SQLLite database on the iPhone inclined me not to, if I could get CoreData working. That particular answer is three years out of date, but I don't recall finding much that was newer which addressed it. Is relative performance and such still an issue? – Bobson Sep 27 '12 at 00:44
  • I don't have any experience with CoreData, but I can't imagine a properly designed ORM+SQLite solution would be slower than CoreData. The ORMs do not add much overhead and tend to be configurable regarding lazy loading. But you do have to design schema with most (perhaps all?) of them. For example, I did have to do a bit of optimizing of my DB layer to be as speedy as possible. I have a very large text column that I lazy load. By moving this column to a secondary table, I improved search and load performance on the non-lazy loaded columns. – t9mike Sep 27 '12 at 04:28
  • If you strike out here, maybe the [MonoTouch mailing list](http://lists.ximian.com/mailman/listinfo/monotouch) could help. Or Xamarin's [support page](http://support.xamarin.com/customer/portal/emails/new). – t9mike Sep 27 '12 at 04:29
  • @t9mike - That doesn't seem so bad. I might just go with that route - the cross-platformness of it is a very good point. – Bobson Sep 27 '12 at 12:09
  • Yup, this would leave you a lot of flexibility for future platforms. I was able to share my data layer between client and server by going this route. And I am now working on Android version of app, so another big win ("It just worked" ;-). – t9mike Sep 27 '12 at 15:11
  • @t9mike - Come to think of it, that would also get around the issue I'm having where the MonoTouch CoreData libraries and the MonoMac CoreData libraries can't both be referenced in the same project. (The Mac app populates the database and the iOS app would consume it.) – Bobson Sep 27 '12 at 15:16
  • @t9mike you should post that as an answer that Bobson could accept... – kdmurray Sep 29 '12 at 23:45
  • @kdmurray - I'm not entirely sure that it actually addresses the question, since it's a workaround that may not work for others, but I would still accept it. – Bobson Sep 30 '12 at 01:48
  • @Bobson I realize it's not the perfect answer but since it's the closest we got I'll award the bounty there if the answer shows up... – kdmurray Sep 30 '12 at 05:11
  • @t9mike - You missed the bounty, but I'll still accept it if you post that suggestion as an answer. – Bobson Oct 10 '12 at 03:48

3 Answers3

1

It isn't very difficult to use CoreData in MonoTouch. You need to generate the mom using momc.exe. I have successfully done it.

If you place the file on the top level of the application directory (maybe other locations will work too) and you set the build action to "BundleResource" (possibly other actions work too). Instantiating a UIManagedDocument will cause CoreData to automatically pick up the mom file, so you won't need to create it yourself, plus you gain the advantage of iCloud support.

You will have to generate the class files yourself. It should be easy to write a script that can read off a xcdatamodel file and spit out the C#. This is an example of how it should look.

public partial class Photographer : NSManagedObject
{
    public static NSString NameKey = (NSString) "name";
    public static NSString PhotosKey = (NSString) "photos";

    public Photographer(IntPtr handle) : base(handle)
    {
    }

    public string Name
    {
        get { return (NSString) Runtime.GetNSObject(ValueForKey(NameKey)); }
        set { SetValueForKey(value, NameKey); }
    }

    public NSSet Photos
    {
        get { return (NSSet) Runtime.GetNSObject(ValueForKey(PhotosKey)); }
        set { SetValueForKey(value, PhotosKey); }
    }

    void SetValueForKey(string value, NSString key)
    {
        base.SetValueForKey((NSString)(value ?? ""), key);
    }

    public static Photographer InsertNewObject(NSManagedObjectContext context)
    {
        return (Photographer) NSEntityDescription.InsertNewObjectForEntityForName("Photographer", context);
    }

    public static Photographer WithName(string name, NSManagedObjectContext context)
    {
        Photographer photographer = null;

        // This is just like Photo(Flickr)'s method.  Look there for commentary.

        if (name.Length > 0)
        {
            var request = new NSFetchRequest("Photographer")
            {
                SortDescriptors = new[] {new NSSortDescriptor("name", true, new Selector("localizedCaseInsensitiveCompare:"))},
                Predicate =  NSPredicate.FromFormat("name = %@", new NSObject[] {(NSString) name})
            };

            NSError error;
            var matches = context.ExecuteFetchRequest(request, out error);

            if (matches == null || matches.Length > 1)
            {
                // handle error
            }
            else if (matches.Length == 0)
            {
                photographer = InsertNewObject(context);
                photographer.Name = name;
            }
            else
            {
                photographer = (Photographer) matches.First();
            }
        }

        return photographer;
    }
}

public partial class Photo : NSManagedObject
{
    public static NSString ImageUrlKey = (NSString) "imageURL";
    public static NSString TitleKey = (NSString) "title";
    public static NSString UniqueKey = (NSString) "unique";
    public static NSString SubtitleKey = (NSString) "subtitle";
    public static NSString WhoTookKey = (NSString) "whoTook";

    public Photo (IntPtr handle) : base (handle)
    {
    }

    public string ImageUrl  {
        get { return (NSString)Runtime.GetNSObject(ValueForKey(ImageUrlKey)); }
        set { SetValueForKey(value, ImageUrlKey); }
    }

    public string Subtitle  {
        get { return (NSString)Runtime.GetNSObject(ValueForKey(SubtitleKey)); }
        set { SetValueForKey(value, SubtitleKey); }
    }

    public string Title  {
        get { return (NSString)Runtime.GetNSObject(ValueForKey(TitleKey)); }
        set { SetValueForKey(value, TitleKey); }
    }

    public string Unique  {
        get { return (NSString)Runtime.GetNSObject(ValueForKey(UniqueKey)); }
        set { SetValueForKey(value, UniqueKey); }

    }

    public Photographer WhoTook  {
        get { return (Photographer)Runtime.GetNSObject(ValueForKey(WhoTookKey)); }
        set { SetValueForKey(value, WhoTookKey); }
    }

void SetValueForKey(string value, NSString key)
{
    base.SetValueForKey((NSString) (value??""), key);
}

    public static Photo InsertNewObject(NSManagedObjectContext context)
    {
        return (Photo) NSEntityDescription.InsertNewObjectForEntityForName("Photo", context);
    }

    public UIImage Image
    {
        get {
            if (string.IsNullOrEmpty(ImageUrl))
                return null;

            var imageData = NSData.FromUrl(new NSUrl(ImageUrl));
            if (imageData == null)
                return null;
            return new UIImage(imageData);
        }
    }
Wesner Moise
  • 261
  • 2
  • 3
  • It's been a while since I've tried to work with this - I'd given up and switched to a SQLLite ORM. But I'll give this a try the next time I work on the project, because I'd love to switch back to CoreData. – Bobson Apr 25 '13 at 16:01
  • Key points: Add a Register attribute to identify the ObjC class name and a constructor with the IntPtr argument, so that Monotouch can create C# classes on the fly. Avoid adding instance fields to the C# class (except those that cache values that can be easily recomputed), because new C# object instances may be created for the same row each time you fetch. Properties should call ValueForKey and SetValueForKey to utilize the underlying dynamic property support offered by NSManagedObject. The Messaging.objc_send functions are useful for reducing gc allocations. – Wesner Moise May 03 '13 at 01:40
  • Also, write a script that reads the xcdatamodel file to generate the partial classes. The xcdatamodel file is easy-to-read XML. Also for collection properties you can create typed ICollections<> in place of NSSet. – Wesner Moise May 03 '13 at 01:45
0

I believe this is what you are after:

public partial class Item : NSManagedObject
{
   internal Item(string entityName)
      : base(NSEntityDescription)m_managedObjectModel.EntitiesByName.ObjectForKey(new NSString(entityName)), m_managedObjectContext)

Where entityName is the name from the MOM.

TrustMe
  • 261
  • 1
  • 3
  • I don't think that's it, or at least not all of it - I still can't access the data fields on Item. I'll clarify the question. – Bobson Sep 25 '12 at 22:23
0

As per @t9mike's comment (which I will accept if he ever makes it an answer), I gave up on using CoreData in favor of a more cross-platform approach. I ended up using Vici CoolStorage for the ORM, although I had to embed the source in my project in order to get it working.

Bobson
  • 13,498
  • 5
  • 55
  • 80