I'm trying to write my first Core Data driven document-oriented OSX application in Swift, as I want to get a bit more into Mac programming. The documents should be saved as XML files only, which I've managed to successfully configure.
The problem is that I want to pre-add some information to the document upon initializing without really "modifying" the document. The information is just required and should be in the document, but if the user creates and right away closes a document he should not be asked to save.
I've defined my Core Data data model and I've created NSManagedObject subclasses from my model. In my document's init method I'm doing this:
override init() {
super.init()
// Add your subclass-specific initialization here.
let newItem = NSEntityDescription.insertNewObjectForEntityForName("MyItem", inManagedObjectContext: self.managedObjectContext) as! MyItem;
newItem.descriptionText = "Item number 1";
newItem.itemNumber = 1;
}
This does add a new item to my document and when I save and re-load I can verify that the item is there. However, doing it this way, the document is marked "dirty" and upon closing it, the user is asked to save the changes.
How would I perform an initialization of my data model without actually marking the document as edited?
For those interested, I managed to solve this to work in my case. First of all, I moved my intialization code to
convenience init?(type typeName: String, error outError: NSErrorPointer)
Then I temporarily suspend the undo-functionality:
self.managedObjectContext.processPendingChanges();
self.undoManager.disableUndoRegistration();
After I make my changes I re-enable the undo-functionality:
self.managedObjectContext.processPendingChanges();
self.undoManager.enableUndoRegistration();
This has worked well so far.
Related
I have a mapping model problem in CoreData with Xcode 13.4 and Swift 5.
Originally when I created the entities in CoreData, one of them had an attribute that was defined as
street_no String
Once I realized that I meant to define it as an Int64, I went and changed it, deleted all the two class files, regenerated the code, and my app would always crash with the following error:
[error] error:
addPersistentStoreWithType:configuration:URL:options:error: returned
error NSCocoaErrorDomain (134140)
in the AppDelegate function:
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: “Invoice_Gen")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
If I changed the attribute back to String, everything worked fine.
After much Googling, the long winded solution I found was:
Keep the attribute, street_no, I want to change as it was originally defined (string)
Create a new attribute called, street_no_2, and define it as an Int64
Delete the two class files for the entity containing the attribute
Regenerate the class files for the entity manually from the Editor menu
Clean Project Folder
Build & Run
Delete the original attribute, street_no
Delete the two class files for the entity containing the attribute
Regenerate the class files for the entity manually from the Editor menu
Clean Project Folder
Build & Run
Rename street_no_2 to street_no
Delete the two class files for the entity containing the attribute
Regenerate the class files for the entity manually from the Editor menu
Clean Project Folder
Build & Run
While I am sure the solution above can be shortened, obviously the problem has to do with the mapping model.
Surely there must be a way to just change an attribute's type?
The error comes because of a mismatch between the stored database file, where you have a string, and what you've told Core Data is in there, which is now an Int. You refer to a mapping model problem but it doesn't sound like you've used a mapping model at all, just changed the model definition directly and expected the framework to cope.
If this is an early stage app (i.e you're still developing the first version) just delete the app from the device or simulator and re-run once you've changed the model and class definition files.
If your app is out in the wild and on user devices, and you don't care about keeping the data, you'll need to include some code to delete the database file on startup according to some defaults key that you define.
If you want to keep the data, then you need to create a new model version in the core data model editor, where the type is defined how you like it, and then add migration or mapping rules to define what should happen during the conversion - I don't think there is any automatically inferrable mapping rule to turn any string into an integer.
I am programmatically setting up a cluster resource (specifically, a Generic Service), using the Windows MI API (Microsoft.Management.Infrastructure).
I can add the service resource just fine. However, my service requires the "Use Network Name for computer name" checkbox to be checked (this is available in the Cluster Manager UI by looking at the Properties for the resource).
I can't figure out how to set this using the MI API. I have searched MSDN and multiple other resources for this without luck. Does anybody know if this is possible? Scripting with Powershell would be fine as well.
I was able to figure this out, after a lot of trial and error, and the discovery of an API bug along the way.
It turns out cluster resource objects have a property called PrivateProperties, which is basically a property bag. Inside, there's a property called UseNetworkName, which corresponds to the checkbox in the UI (and also, the ServiceName property, which is also required for things to work).
The 'wbemtest' tool was invaluable in finding this out. Once you open the resource instance in it, you have to double-click the PrivateProperties property to bring up a dialog which has a "View Embedded" button, which is then what shows you the properties inside. Somehow I had missed this before.
Now, setting this property was yet another pain. Due to what looks like a bug in the API, retrieving the resource instance with CimSession.GetInstance() does not populate property values. This misled me into thinking I had to add the PrivateProperties property and its inner properties myself, which only resulted in lots of cryptic errors.
I finally stumbled upon this old MSDN post about it, where I realized the property is dynamic and automatically set by WMI. So, in the end, all you have to do is know how to get the property bag using CimSession.QueryInstances(), so you can then set the inner properties like any other property.
This is what the whole thing looks like (I ommitted the code for adding the resource):
using (var session = CimSession.Create("YOUR_CLUSTER", new DComSessionOptions()))
{
// This query finds the newly created resource and fills in the
// private props we'll change. We have to do a manual WQL query
// because CimSession.GetInstance doesn't populate prop values.
var query =
"SELECT PrivateProperties FROM MSCluster_Resource WHERE Id=\"{YOUR-RES-GUID}\"";
// Lookup the resource. For some reason QueryInstances does not like
// the namespace in the regular form - it must be exactly like this
// for the call to work!
var res = session.QueryInstances(#"root/mscluster", "WQL", query).First();
// Add net name dependency so setting UseNetworkName works.
session.InvokeMethod(
res,
"AddDependency",
new CimMethodParametersCollection
{
CimMethodParameter.Create(
"Resource", "YOUR_NET_NAME_HERE", CimFlags.Parameter)
});
// Get private prop bag and set our props.
var privProps =
(CimInstance)res.CimInstanceProperties["PrivateProperties"].Value;
privProps.CimInstanceProperties["ServiceName"].Value = "YOUR_SVC_HERE";
privProps.CimInstanceProperties["UseNetworkName"].Value = 1;
// Persist the changes.
session.ModifyInstance(#"\root\mscluster", res);
}
Note how the quirks in the API make things more complicated than they should be: QueryInstances expects the namespace in a special way, and also, if you don't add the network name dependency first, setting private properties fails silently.
Finally, I also figured out how to set this through PowerShell. You have to use the Set-ClusterParameter command, see this other answer for the full info.
I found a major problem with the architecture of my Document based app.
Basically a store the model (a simple string) in a global variable, every time the text in field changes. I have the document save this string as it's data, and restore re-opened files using this data.
Now, the major problem that I now see is that if I restore any saved file, I populate the global variable from the document in the documents "readFromData" function (works).
But if I create a new document, "readFromData" is never called, so I have no way to set the global string to "", and thus my new documents global variable is still populated with the last saved string. (I use this to put the string back into the text view on load.
So as a simple workaround, I would need to be able to use a function that is automatically called and only ever called by the creation of a new document, to set my global variable back to "".
I can not find such a function I can override. Does one exist..?
I am not sure I understand what you are trying to do.
You could use this NSDocument initializer:
/* Initialize a new empty document of a specified type,
and return it if successful.
…
You can override this method to perform initialization that
must be done when creating new documents but should not be done
when opening existing documents.
*/
- (instancetype)initWithType:(NSString *)typeName error:(NSError **)outError;
This is invoked exactly once per document at the initial creation of the document. It will not be invoked when a document is opened after being saved to disk.
In my project I need to be able to tell the difference between documents created by the user and those restored at application launch by restoreStateWithCoder because there are some thing s that need to be done for new documents, but not restored ones. How can I do this?
How about subclassing "NSDocument" and using that subclass for your document?
Then, you can catch "restoreStateWithCoder" as it happens and set a unique flag (e.g. a BOOL property) for those documents that are restored from disk and not created fresh via "File -> New" command.
You can also attempt to "method swizzle" "restoreStateWithCoder", but you have to decide what property to set in which object.
[Answering this for Swift, but the general idea works for Objective-C as well]
When a document is brand new, you generally get a call to the following function:
convenience init(type tyepName: String) throws
You could set a flag in that function (say needSpecialHandling = true, a variable which is originally initialised to false) to say whether you need some special handling for such cases.
Then in the makeWindowControllers() function you use that variable to trigger invoking the special code (if true) the same way you invoked it possibly in the windowControllerDidLoadNib function.
I get System.NotSupportedException: An attempt has been made to Attach or Add an entity that is not new perhaps having been loaded from another DataContext when I want to update an object's with child entities.
The scenario is like this:
I have a SubscriberProvider that allows me to create subscribers.
var provider = new SubscriberProvider(); // Creates a new repository with own datacontext
var newSubscriber = new Subscriber
{
EmailAddress = emailAddress,
};
newSubscriber.Interests.Add(new Interest{
Id=1,
Name="cars"
});
provider.Subscribe(newSubscriber);
On a normal subscribe page, this works fine.
Now I have a linq2sql Member class(retrievable by a MemberRepository) and I want to extend it to have a helper subscribe method like so:
var repository = new MembershipRepository(); // Holds its own datacontext
var member = repository.Get("member1");
member.Subscribe(); // transfer member's info and interests to subscriber's table
The exception occurs when SubscriberProvider tries to add interests of the member.
Commenting out
newSubscriber.Interests.Add(new Interest{
Id=1,
Name="cars"
});
will make member.Subscribe() work.
member.Subscribe() is simply:
public void Subscribe(bool emailIsVerified, bool receiveEmails, bool sendDoubleOptIn)
{
var provider = new MailingListProvider();
provider.Subscribe(EmailAddress, emailIsVerified, receiveEmails, CountryId, sendDoubleOptIn, ConvertInterests(MemberInterests.ToList()));
}
So what's causing the child entities(Interests) to lose their datacontext when I do member.Subscribe() and how do I go about fixing this?
It seems there's some code missing here, but I'll take a stab anyway because I think I have an idea what's going on.
If you have a different DataContext created for your MembershipRepository and your SubscriberRepository you're going to have issues related to entities "having been loaded from another DataContext." (as the Exception you posted points out). You can't just take an object out of one DataContext and save it into another.
It seems that you might have an architectural issue here. Should these 2 repositories actually be separate? If so, should they have completely different DataContexts? I would probably recommend using Dependency Injection to inject your DataContexts into your Repositories. Then you can decide how to cache your DataContexts.
That line of code you commented out is being flagged by the DataContext as a new record, even though it's likely that the record already exists, due to the error message.
Change the line to:
newSubscriber.Interests.Add(DataContext.Interests.Where(a => a.Id == 1).Single());
Now, the DataContext will know that record is one that already exists, and won't try to add it as an Insert to the ChangeSet.
Found the solution to this myself. Turns out it was the ConvertInterests() method causing it. The converted interest object had an invalid declaration which compiled ok.
Thinking the code was simple enough, I didn't create a test for it. I should have known better!