Synchronize IOS core data with web service? - web-services

Here is my problem:
I want to use core data - speed and connectivity issues to build my IOS app. The data stored in core data is coming from a SQLServer database which I can access through a yet-to-be-defined web service.
Any changes to the data stored in core data needs to be synchronized with the SQLServer via a web service. In addition, I need to buffer changes that don't get synchronized because of connectivity issues.
I also need to update core data with any changes that have occured on the server. This could happen on a schedule set in user preferences.
Solutions I've Explored:
Using NSIncrementalStore class (new in IOS 5). I'm very confused on what this does exactly but it sounds promising. From what I can tell, you subclass NSIncrementalStore which allows you to intercept the regular core data API calls. I could then pass on the the information to core data as well as sync it with the external database via a web service. I could be completely wrong. But assuming I'm right, how would I sync deltas if the connection to the internet is down?
AFIncrementalStore - This is a subclass off of NSIncrementalStore using AFNetworking to do the web services piece.
RestKit - I'm a little concerned on how active this API is and it seems to be going through a transition to block functionality. Has anyone used this extensively?
I'm leaning towards AFIncrementalStore since this is using (what seems to be) a more standard approach. The problem is, I could be completely off on what NSIncrementalStore really is.
A link to some sample code or tutorial would be great!

My solution to this was to store two copies of the data set in a CoreData database. One represents the last-known server state and is immutable. The other is edited by the user.
When it is time to sync changes, the app creates a diff between the edited and immutable copies of the data. The app sends the diff to a web service which applies the diff to its own copy of the data. It replies with a full copy of the data set, which the app overwrites onto both of its copies of the data.
The advantages are:
If there is no network connectivity, no changes are lost: the diff is calculated each time the data set needs to be sent, and the immutable copy is only changed on a successful sync.
Only the minimum amount of information that needs to be sent is transmitted.
Multiple people can edit the same data at the same time without using locking strategies with a minimum opportunity for data loss via overwrites.
The disadvantages are:
Writing the diffing code is complex.
Writing the merging service is complex.
Unless you are a metaprogramming guru, you'll find that your diff/merge code is brittle and has to change whenever you change your object model.
Here are some of the considerations I had when coming up with the strategy:
If you allow changes to be made offline, checkin/checkout locking won't work (how can you establish a lock with no connection?).
What happens if two people edit the same data at the same time?
What happens if one person edits data on one iOS device when connectionless, switches it off, edits on another device and then turns the original device back on?
Multithreading with CoreData is an entire problem class in itself.
The closest thing I've heard of to out-of-the-box support to do anything remotely like this is the new iCloud/CoreData syncing system in iOS6, which automatically transmits entities from a CoreData database to iCloud when they change. However, that means you have to use iCloud.
EDIT: This is very late, I know, but here's a class that is capable of producing a diff between two NSManagedObject instances.
// SZManagedObjectDiff.h
#interface SZManagedObjectDiff
- (NSDictionary *)diffNewObject:(NSManagedObject *)newObject withOldObject:(NSManagedObject *)oldObject
#end
// SZManagedObjectDiff.m
#import "SZManagedObjectDiff.h"
#implementation SZManagedObjectDiff
- (NSDictionary *)diffNewObject:(NSManagedObject *)newObject withOldObject:(NSManagedObject *)oldObject {
NSDictionary *attributeDiff = [self diffAttributesOfNewObject:newObject withOldObject:oldObject];
NSDictionary *relationshipsDiff = [self diffRelationshipsOfNewObject:newObject withOldObject:oldObject];
NSMutableDictionary *diff = [NSMutableDictionary dictionary];
if (attributeDiff.count > 0) {
diff[#"attributes"] = attributeDiff;
}
if (relationshipsDiff.count > 0) {
diff[#"relationships"] = relationshipsDiff;
}
if (diff.count > 0) {
diff[#"entityName"] = newObject ? newObject.entity.name : oldObject.entity.name;
NSString *idAttributeName = newObject ? newObject.entity.userInfo[#"id"] : oldObject.entity.userInfo[#"id"];
if (idAttributeName) {
id itemId = newObject ? [newObject valueForKey:idAttributeName] : [oldObject valueForKey:idAttributeName];
if (itemId) {
diff[idAttributeName] = itemId;
}
}
}
return diff;
}
- (NSDictionary *)diffRelationshipsOfNewObject:(NSManagedObject *)newObject withOldObject:(NSManagedObject *)oldObject {
NSMutableDictionary *diff = [NSMutableDictionary dictionary];
NSDictionary *relationships = newObject == nil ? [[oldObject entity] relationshipsByName] : [[newObject entity] relationshipsByName];
for (NSString *name in relationships) {
NSRelationshipDescription *relationship = relationships[name];
if (relationship.deleteRule != NSCascadeDeleteRule) continue;
SEL selector = NSSelectorFromString(name);
id newValue = nil;
id oldValue = nil;
if (newObject != nil && [newObject respondsToSelector:selector]) newValue = [newObject performSelector:selector];
if (oldObject != nil && [oldObject respondsToSelector:selector]) oldValue = [oldObject performSelector:selector];
if (relationship.isToMany) {
NSArray *changes = [self diffNewSet:newValue withOldSet:oldValue];
if (changes.count > 0) {
diff[name] = changes;
}
} else {
NSDictionary *relationshipDiff = [self diffNewObject:newValue withOldObject:oldValue];
if (relationshipDiff.count > 0) {
diff[name] = relationshipDiff;
}
}
}
return diff;
}
- (NSDictionary *)diffAttributesOfNewObject:(NSManagedObject *)newObject withOldObject:(NSManagedObject *)oldObject {
NSMutableDictionary *diff = [NSMutableDictionary dictionary];
NSArray *attributeNames = newObject == nil ? [[[oldObject entity] attributesByName] allKeys] : [[[newObject entity] attributesByName] allKeys];
for (NSString *name in attributeNames) {
SEL selector = NSSelectorFromString(name);
id newValue = nil;
id oldValue = nil;
if (newObject != nil && [newObject respondsToSelector:selector]) newValue = [newObject performSelector:selector];
if (oldObject != nil && [oldObject respondsToSelector:selector]) oldValue = [oldObject performSelector:selector];
newValue = newValue ? newValue : [NSNull null];
oldValue = oldValue ? oldValue : [NSNull null];
if (![newValue isEqual:oldValue]) {
diff[name] = #{ #"new": newValue, #"old": oldValue };
}
}
return diff;
}
- (NSArray *)diffNewSet:(NSSet *)newSet withOldSet:(NSSet *)oldSet {
NSMutableArray *changes = [NSMutableArray array];
// Find all items that have been newly created or updated.
for (NSManagedObject *newItem in newSet) {
NSString *idAttributeName = newItem.entity.userInfo[#"id"];
NSAssert(idAttributeName, #"Entities must have an id property set in their user info.");
id newItemId = [newItem valueForKey:idAttributeName];
NSManagedObject *oldItem = nil;
for (NSManagedObject *setItem in oldSet) {
id setItemId = [setItem valueForKey:idAttributeName];
if ([setItemId isEqual:newItemId]) {
oldItem = setItem;
break;
}
}
NSDictionary *diff = [self diffNewObject:newItem withOldObject:oldItem];
if (diff.count > 0) {
[changes addObject:diff];
}
}
// Find all items that have been deleted.
for (NSManagedObject *oldItem in oldSet) {
NSString *idAttributeName = oldItem.entity.userInfo[#"id"];
NSAssert(idAttributeName, #"Entities must have an id property set in their user info.");
id oldItemId = [oldItem valueForKey:idAttributeName];
NSManagedObject *newItem = nil;
for (NSManagedObject *setItem in newSet) {
id setItemId = [setItem valueForKey:idAttributeName];
if ([setItemId isEqual:oldItemId]) {
newItem = setItem;
break;
}
}
if (!newItem) {
NSDictionary *diff = [self diffNewObject:newItem withOldObject:oldItem];
if (diff.count > 0) {
[changes addObject:diff];
}
}
}
return changes;
}
#end
There's more information about what it does, how it does it and its limitations/assumptions here:
http://simianzombie.com/?p=2379

Use the Parse platform and its IOS SDK to structure and store info. It can cache data locally so you can retrieve it quickly and when there is no connectivity.

Related

Glass Mapper V4 Language Item Fallback, how to make it work with Autofac?

I am using Glass Mapper v4 with Autofac and cant figure out how to make it work with the Language Item Fall back module. There are examples of creating a class that implements IObjectConstructionTask (see below)
public class FallbackCheckTask : IObjectConstructionTask
{
public void Execute(ObjectConstructionArgs args)
{
if (args.Result == null)
{
var scContext = args.AbstractTypeCreationContext as SitecoreTypeCreationContext;
// if the item itself is null, regardless of version, abort
if (scContext.Item == null)
{
args.AbortPipeline();
return;
}
// we could be trying to convert rendering parameters to a glass model, and if so, just return.
if (String.Compare(scContext.Item.Paths.FullPath, "[orphan]/renderingParameters", true) == 0)
{
return;
}
// the default glassmapper code would simply abort pipeline if the context items version count for the current langauge was 0
// but this does not take item fallback into account
// added here a check on the fallback extension method GetFallbackItem, recursively (for chained fallback)
// and then if that fallback item is null or it's version count is 0 (and only then) would you go ahead and abort the pipeline
if (scContext.Item.Versions.Count == 0)
{
var fallBackItem = CheckRecursivelyForFallbackItem(scContext.Item);
if (fallBackItem == null)
args.AbortPipeline();
else if (fallBackItem.Versions.Count == 0)
args.AbortPipeline();
return;
}
}
}
// in the case of chained fallback, eg fr-CA -> en-CA -> en
// could be that the middle languages don't have versions either, but DO have a fallback item
// therefore, must check back further until either a version is found, or there are no more fallback items
private Item CheckRecursivelyForFallbackItem(Item thisItem)
{
var fallBackItem = thisItem.GetFallbackItem();
if (fallBackItem != null)
{
if (fallBackItem.Versions.Count == 0)
fallBackItem = CheckRecursivelyForFallbackItem(fallBackItem);
}
return fallBackItem;
}
}
Then you register (with Castle Windsor)
public static void CastleConfig(IWindsorContainer container){
var config = new Config();
container.Register(
Component.For<IObjectConstructionTask>().ImplementedBy<FallbackCheckTask>().LifestylePerWebRequest()
);
// config.EnableCaching = false;
container.Install(new SitecoreInstaller(config));
}
I am using Autofac and do not know how to perform the same action as above and assure it happens in the right order. I am registering my types the typical way (See below) but it doesn't seem to be hooking my FallbackCheckTask class.
public static void Register()
{
var builder = new ContainerBuilder();
builder.RegisterControllers(typeof(MvcApplication).Assembly);
// register our types
builder.RegisterType<FallbackCheckTask>().As<IObjectConstructionTask>().InstancePerLifetimeScope();
// build and set the resolver
var container = builder.Build();
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
}
I also have the Language Item Fallback wired up and working as expected if glass is not involved in fetching the items values. I understand why Glass is not mapping the data out of the box, I just cant seem to get the fix working. Any thoughts?
EDIT 2015-05-21 19:00
I edited GlassMapperScCustom.cs as follows:
public static IDependencyResolver CreateResolver(){
var config = new Glass.Mapper.Sc.Config();
var resolver = new DependencyResolver(config);
resolver.ObjectConstructionFactory.Add(() => new FallbackCheckTask());
return resolver;
}
And now its calling the Execute method of the the FallbackCheckTask only if there is a version of the item, if there is no version its not calling the method. Also, no matter what I do if I enable this Task my test query items always come back as NULL:
var test = SitecoreContext.QuerySingle<Item>("{7A6D933A-127B-4C08-B073-7C39F16EBD06}");
var test1 = SitecoreContext.Query<Item>("{7A6D933A-127B-4C08-B073-7C39F16EBD06}").ToList();
var test2 = SitecoreContext.GetCurrentItem<Item>();
var test3 = SitecoreContext.GetItem<Item>("{7A6D933A-127B-4C08-B073-7C39F16EBD06}");
So to sum it up, I am a little better off now then I was before but when the task class is registered queries come back as null for all items no matter if they have a version or not. As mentioned before, any help is appreciated.
I know you mentioned that you were using the VersionCountDisabler but does it look like this:
using(new VersionCountDisabler()){
var test = SitecoreContext.QuerySingle<Item>("{7A6D933A-127B-4C08-B073-7C39F16EBD06}");
var test1 = SitecoreContext.Query<Item>("{7A6D933A-127B-4C08-B073-7C39F16EBD06}").ToList();
var test2 = SitecoreContext.GetCurrentItem<Item>();
var test3 = SitecoreContext.GetItem<Item>("{7A6D933A-127B-4C08-B073-7C39F16EBD06}");
}
Or are you disabling it in some other manner?
I also notice that your fallback code doesn't seem to update the scContent.Item property. I think you need to update it to the following:
public class FallbackCheckTask : IObjectConstructionTask
{
public void Execute(ObjectConstructionArgs args)
{
if (args.Result == null)
{
var scContext = args.AbstractTypeCreationContext as SitecoreTypeCreationContext;
// if the item itself is null, regardless of version, abort
if (scContext.Item == null)
{
args.AbortPipeline();
return;
}
// we could be trying to convert rendering parameters to a glass model, and if so, just return.
if (String.Compare(scContext.Item.Paths.FullPath, "[orphan]/renderingParameters", true) == 0)
{
return;
}
// the default glassmapper code would simply abort pipeline if the context items version count for the current langauge was 0
// but this does not take item fallback into account
// added here a check on the fallback extension method GetFallbackItem, recursively (for chained fallback)
// and then if that fallback item is null or it's version count is 0 (and only then) would you go ahead and abort the pipeline
if (scContext.Item.Versions.Count == 0)
{
var fallBackItem = CheckRecursivelyForFallbackItem(scContext.Item);
if (fallBackItem == null)
args.AbortPipeline();
else if (fallBackItem.Versions.Count == 0)
args.AbortPipeline();
//don't just return but update the scContext.Item to the fallback item
scContext.Item = fallbackItem;
}
}
}
// in the case of chained fallback, eg fr-CA -> en-CA -> en
// could be that the middle languages don't have versions either, but DO have a fallback item
// therefore, must check back further until either a version is found, or there are no more fallback items
private Item CheckRecursivelyForFallbackItem(Item thisItem)
{
var fallBackItem = thisItem.GetFallbackItem();
if (fallBackItem != null)
{
if (fallBackItem.Versions.Count == 0)
fallBackItem = CheckRecursivelyForFallbackItem(fallBackItem);
}
return fallBackItem;
}
}

Sitecore change rendering datasource

Hello I would like create special page for private use for where i will be have possibility to change data source value for each rendering item.
I have created next code, but it dos't save any changes to items.
SC.Data.Database master = SC.Configuration.Factory.GetDatabase("master");
SC.Data.Items.Item itm = master.GetItem(tbPath.Text);
if (itm != null)
{
// Get the sublayout to update
//string sublayout_name = txtSublayout.Text.Trim();
//if (!string.IsNullOrEmpty(sublayout_name))
{
// Get the renderings on the item
RenderingReference[] refs = itm.Visualization.GetRenderings(SC.Context.Device, true);
if (refs.Any())
{
//var data = refs.Select(d=>d);
//refs[0].Settings.DataSource
var sb = new StringBuilder();
using (new SC.SecurityModel.SecurityDisabler())
{
itm.Editing.BeginEdit();
foreach (var d in refs)
{
if (d.Settings.DataSource.Contains("/sitecore/content/Site Configuration/"))
{
var newds = d.Settings.DataSource.Replace("/sitecore/content/Site Configuration/", "/sitecore/content/Site Configuration/" + tbLanguage.Text + "/");
// sb.AppendLine(string.Format("{0} old: {1} new: {2}<br/>", d.Placeholder, d.Settings.DataSource, newds));
d.Settings.DataSource = newds;
}
}
itm.Editing.EndEdit();
}
//lblResult.Text = sb.ToString();
}
}
}
how I can change data source ?
thanks
You're mixing up two different things in Sitecore here.
The datasource that is assigned to a rendering at run-time, when Sitecore is rendering a page
The datasource that is assigned to the presentation details of an item
The simplest approach to achieve what I think you're trying to achieve, would be this.
Item itm = database.GetItem("your item");
string presentationXml = itm["__renderings"];
itm.Editing.BeginEdit();
presentationXml.Replace("what you're looking for", "what you want to replace it with");
itm.Editing.EndEdit();
(I've not compiled and run this code, but it should pretty much work as is)
You are not saving the changes to the field.
Use the LayoutDefinitation class to parse the layout field, and foreach all the device definition, and rendering definitions.
And finaly commit the LayoutDifinition to the layout field.
SC.Data.Items.Item itm = master.GetItem(tbPath.Text);
var layoutField = itm.Fields[Sitecore.FieldIDs.LayoutField];
LayoutDefinition layout = LayoutDefinition.Parse(layoutField.Value);
for (int i = 0; i < layout.Devices.Count; i++)
{
DeviceDefinition device = layout.Devices[i] as DeviceDefinition;
for (int j = 0; j < device.Renderings.Count; j++)
{
RenderingDefinition rendering = device.Renderings[j] as RenderingDefinition;
rendering.Datasource = rendering.DataSource.Replace("/sitecore/content/Site Configuration/",
"/sitecore/content/Site Configuration/" + tbLanguage.Text + "/");
}
}
itm.Editing.BeginEdit();
var xml =layout.ToXml()
layoutField.Value = xml;
itm.Editing.EndEdit();
The code is not testet, but are changed from something i have in production to replace datasources on a copy event
If any one looking for single line Linq : (Tested)
var layoutField = item.Fields[Sitecore.FieldIDs.LayoutField];
if (layoutField != null)
{
var layout = LayoutDefinition.Parse(layoutField.Value);
if (layout != null)
{
foreach (var rendering in layout.Devices.Cast<DeviceDefinition>()
.SelectMany
(device => device.Renderings.Cast<RenderingDefinition>()
.Where
(rendering => rendering.ItemID == "RenderingYoulooking")))
{
rendering.Datasource = "IDYouWantToInsert";
}
layoutField.Value = layout.ToXml();
}
}

Replicating Google Analytics DateRange picker

I need to replicate the Google Analytics date picker (plus a few new options). Can anyone tell me how to highlight all the cells on a calendar between two dates. My basic JavaScript is OK but I think I'm getting a bit out of my depth.
I'm using JQuery 1.5.1 and JQuery UI 1.8.14.
In needed to replicate Google Analytics date picker as well. I know you were asking just about highlighting cells, but if someone else would prefer complete solution, you can see my answer from another question: jquery google analytics datepicker
Here's a solution using the built-in 'onSelect' event (jsFiddle):
$(document).ready(function() {
'use strict';
var range = {
'start': null,
'stop': null
};
$('#picker').datepicker({
'onSelect': function(dateText, inst) {
var d, ds, i, sel, $this = $(this);
if (range.start === null || range.stop === null) {
if (range.start === null) {
range.start = new Date(dateText);
} else {
range.stop = new Date(dateText);
}
}
if (range.start !== null && range.stop !== null) {
if ($this.find('td').hasClass('selected')) {
//clear selected range
$this.children().removeClass('selected');
range.start = new Date(dateText);
range.stop = null;
//call internal method '_updateDatepicker'.
inst.inline = true;
} else {
//prevent internal method '_updateDatepicker' from being called.
inst.inline = false;
if (range.start > range.stop) {
d = range.stop;
range.stop = range.start;
range.start = d;
}
sel = (range.start.toString() === range.stop.toString()) ? 0 : (new Date(range.stop - range.start)).getDate();
for (i = 0; i <= sel; i += 1) {
ds = (range.start.getMonth() + 1).toString() + '/' + (range.start.getDate() + i).toString() + '/' + (range.start.getFullYear()).toString();
d = new Date(ds);
$this.find('td a').filter(function(index) {
return $(this).text() === d.getDate().toString();
}).parents('td').addClass('selected');
}
}
}
}
});
});
I became desperate and came up with a solution on my own. It wasn't pretty but I'll detail it.
I was able to construct a div that had the text boxes, buttons and the datepicker that looked like the Google Analytics control but I couldn't make the datepicker work properly. Eventually, I came up with the idea of creating a toggle variable that kept track of which date you were selecting (start date or end date). Using that variable in a custom onSelect event handler worked well but I still couldn't figure out how to get the cells between dates to highlight.
It took a while, but I slowly came to the realization that I couldn't do it with the datepicker as it existed out of the box. Once I figured that out, I was able to come up with a solution.
My solution was to add a new event call afterSelect. This is code that would run after all the internal adjustments and formatting were complete. I then wrote a function that, given a cell in the datepicker calendar, would return the date that it represented. I identified the calendar date cells by using jQuery to find all the elements that had the "ui-state-default" class. Once I had the date function and a list of all the calendar cells, I just needed to iterate over all of them and, if the date was in the correct range, add a new class to the parent.
It was extremely tedious but I was able to make it work.

AppFabric Cache concurrency issue?

While stress testing prototype of our brand new primary system, I run into concurrent issue with AppFabric Cache. When concurrently calling many DataCache.Get() and Put() with same cacheKey, where I attempt to store relatively large objet, I recieve "ErrorCode:SubStatus:There is a temporary failure. Please retry later." It is reproducible by the following code:
var dcfc = new DataCacheFactoryConfiguration
{
Servers = new[] {new DataCacheServerEndpoint("localhost", 22233)},
SecurityProperties = new DataCacheSecurity(DataCacheSecurityMode.None, DataCacheProtectionLevel.None),
};
var dcf = new DataCacheFactory(dcfc);
var dc = dcf.GetDefaultCache();
const string key = "a";
var value = new int [256 * 1024]; // 1MB
for (int i = 0; i < 300; i++)
{
var putT = new Thread(() => dc.Put(key, value));
putT.Start();
var getT = new Thread(() => dc.Get(key));
getT.Start();
}
When calling Get() with different key or DataCache is synchronized, this issue will not appear. If DataCache is obtained with each call from DataCacheFactory (DataCache is supposed to be thread-safe) or timeouts are prolonged it has no effect and error is still received.
It seems to me very strange that MS would leave such bug. Did anybody faced similar issue?
I also see the same behavior and my understanding is that this is by design. The cache contains two concurrency models:
Optimistic Concurrency Model methods: Get, Put, ...
Pessimistic Concurrency Model: GetAndLock, PutAndLock, Unlock
If you use optimistic concurrency model methods like Get then you have to be ready to get DataCacheErrorCode.RetryLater and handle that appropriately - I also use a retry approach.
You might find more information at MSDN: Concurrency Models
We have seen this problem as well in our code. We solve this by overloading the Get method to catch expections and then retry the call N times before fallback to a direct request to SQL.
Here is a code that we use to get data from the cache
private static bool TryGetFromCache(string cacheKey, string region, out GetMappingValuesToCacheResult cacheResult, int counter = 0)
{
cacheResult = new GetMappingValuesToCacheResult();
try
{
// use as instead of cast, as this will return null instead of exception caused by casting.
if (_cache == null) return false;
cacheResult = _cache.Get(cacheKey, region) as GetMappingValuesToCacheResult;
return cacheResult != null;
}
catch (DataCacheException dataCacheException)
{
switch (dataCacheException.ErrorCode)
{
case DataCacheErrorCode.KeyDoesNotExist:
case DataCacheErrorCode.RegionDoesNotExist:
return false;
case DataCacheErrorCode.Timeout:
case DataCacheErrorCode.RetryLater:
if (counter > 9) return false; // we tried 10 times, so we will give up.
counter++;
Thread.Sleep(100);
return TryGetFromCache(cacheKey, region, out cacheResult, counter);
default:
EventLog.WriteEntry(EventViewerSource, "TryGetFromCache: DataCacheException caught:\n" +
dataCacheException.Message, EventLogEntryType.Error);
return false;
}
}
}
Then when we need to get something from the cache we do:
TryGetFromCache(key, region, out cachedMapping)
This allows us to use Try methods that encasulates the exceptions. If it returns false, we know thing is wrong with the cache and we can access SQL directly.

Subsonic 3 Save() then Update()?

I need to get the primary key for a row and then insert it into one of the other columns in a string.
So I've tried to do it something like this:
newsObj = new news();
newsObj.name = "test"
newsObj.Save();
newsObj.url = String.Format("blah.aspx?p={0}",newsObj.col_id);
newsObj.Save();
But it doesn't treat it as the same data object so newsObj.col_id always comes back as a zero. Is there another way of doing this? I tried this on another page and to get it to work I had to set newsObj.SetIsLoaded(true);
This is the actual block of code:
page p;
if (pageId > 0)
p = new page(ps => ps.page_id == pageId);
else
p = new page();
if (publish)
p.page_published = 1;
if (User.IsInRole("administrator"))
p.page_approved = 1;
p.page_section = staticParent.page_section;
p.page_name = PageName.Text;
p.page_parent = parentPageId;
p.page_last_modified_date = DateTime.Now;
p.page_last_modified_by = (Guid)Membership.GetUser().ProviderUserKey;
p.Add();
string urlString = String.Empty;
if (parentPageId > 0)
{
urlString = Regex.Replace(staticParent.page_url, "(.aspx).*$", "$1"); // We just want the static page URL (blah.aspx)
p.page_url = String.Format("{0}?p={1}", urlString, p.page_id);
}
p.Save();
If I hover the p.Save(); I can see the correct values in the object but the DB is never updated and there is no exception.
Thanks!
I faced the same problem with that :
po oPo = new po();
oPo.name ="test";
oPo.save(); //till now it works.
oPo.name = "test2";
oPo.save(); //not really working, it's not saving the data since isLoaded is set to false
and the columns are not considered dirty.
it's a bug in the ActiveRecord.tt for version 3.0.0.3.
In the method public void Add(IDataProvider provider)
immediately after SetIsNew(false);
there should be : SetIsLoaded(true);
the reason why the save is not working the second time is because the object can't get dirty if it is not loaded. By adding the SetIsLoaded(true) in the ActiveRecord.tt, when you are going to do run custom tool, it's gonna regenerate the .cs perfectly.