I'm trying to update an entity using a stub. This works fine for changing records, unless I try to set the value back to the default value forr the column. Eg: If the default value is 0, I can change to and from any value except zero, but the changes aren't saved if I try to set it back to zero. This is the code I'm using:
var package = new Package() {
PackageID = 4
};
...
public static void EditPackage(Package package) {
using(var context = new ShopEntities()) {
context.Packages.MergeOption = MergeOption.NoTracking;
var existing = new Package() {
PackageID = package.PackageID
};
context.AttachTo("Packages", existing);
context.ApplyPropertyChanges("ShopEntities.Packages", package);
context.AcceptAllChanges(); // doesn't make a difference
System.Diagnostics.Debug.WriteLine((package.DateSent.HasValue ? package.DateSent.Value.ToString("D") : "none") + "\t\t" + package.IsReceived);
context.SaveChanges();
}
}
In the example above, DateSent's default value is null (It's a DateTime?), and I can also set it to any value other than null, and the debug line confirms the correct properties are set, they're just not saved. I think I must be missing something.
Thanks for any help.
Turns out what I needed to do was manually mark each property in the new item as modified.
/// <summary>
/// Sets all properties on an object to modified.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="entity">The entity.</param>
private static void SetAllPropertiesModified(ObjectContext context, object entity) {
var stateEntry = context.ObjectStateManager.GetObjectStateEntry(entity);
// Retrieve all the property names of the entity
var propertyNames = stateEntry.CurrentValues.DataRecordInfo.FieldMetadata.Select(fm => fm.FieldType.Name);
foreach(var propertyName in propertyNames) {// Set each property as modified
stateEntry.SetModifiedProperty(propertyName);
}
}
You are creating a new package (with the id of an existing package), which you are calling "existing". You are then attaching it as if it was an existing package. You should load this package from the database and then attach it.
Related
I have a OData V4 over Asp.net WebApi (OWIN).
Everything works great, except when I try to query a 4-level $expand.
My query looks like:
http://domain/entity1($expand=entity2($expand=entity3($expand=entity4)))
I don't get any error, but the last expand isn't projected in my response.
More info:
I've set the MaxExpandDepth to 10.
All my Entities are EntitySets.
I'm using the ODataConventionModelBuilder.
I've opened an SQL-profiler and could see that the query (and the result) is correct. It's some filter that occurs after the query is executed.
I've searched the web and didn't find anything suitable.
I've tried different entity 4 level $expands and they didn't work as well.
Edit:
I've overridden the OnActionExecuted:
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
base.OnActionExecuted(actionExecutedContext);
var objectContent = actionExecutedContext.Response.Content as ObjectContent;
var val = objectContent.Value;
var t = Type.GetType("System.Web.OData.Query.Expressions.SelectExpandWrapperConverter, System.Web.OData");
var jc = Activator.CreateInstance(t) as JsonConverter;
var jss = new JsonSerializerSettings();
jss.Converters.Add(jc);
var ser = JsonConvert.SerializeObject(val, jss);
}
The serialized value contains entity4.
I still have no idea what component removes entity4 in the pipe.
Edit #2:
I've create an adapter over DefaultODataSerializerProvider and over all the other ODataEdmTypeSerializer's. I see that during the process the $expand for entity4 exists and when the ODataResourceSerializer.CreateNavigationLink method is called on that navigationProperty (entity4) then it returns null.
I've jumped into the source code and I could see that the SerializerContext.Items doesn't include the entity4 inside it's items and the SerializerContext.NavigationSource is null.
To be specific with versions, I'm using System.Web.OData, Version=6.1.0.10907.
Ok, so I noticed the problem was due to the fact that my navigation property was of type EdmUnknownEntitySet and the navigation property lookup returns null (source code attached with an evil TODO..):
/// <summary>
/// Finds the entity set that a navigation property targets.
/// </summary>
/// <param name="property">The navigation property.</param>
/// <returns>The entity set that the navigation propertion targets, or null if no such entity set exists.</returns>
/// TODO: change null logic to using UnknownEntitySet
public override IEdmNavigationSource FindNavigationTarget(IEdmNavigationProperty property)
{
return null;
}
So I understood my problem was with the EdmUnknownEntitySet.
I digged into the code and saw that I needed to add the ContainedAttribute to the my navigation properties.
Since my solution is kind of a Generic repository, I've added it in the Startup for All navigation properties:
builder.OnModelCreating = mb => mb.StructuralTypes.SelectMany(s => s.NavigationProperties
.Where(np => np.Multiplicity == EdmMultiplicity.Many)).Distinct().ForEach(np => np.Contained());
//......
var model = builder.GetEdmModel();
I am trying to delete a site-columns from the sharepoint website directly from my code. These site-columns are currently referenced under some content-types. So when I execute a code
//Delete the site-column
conFields.DeleteObject();
clientContext.ExecuteQuery();
break;
it throws an exception
Site columns which are included in content types cannot be deleted. Remove all references to this site column prior to deleting it.
Can anyone please suggest a way to first remove that reference from the content-type and then delete the site-column.
Here's the code:
//availableCT is my content-type
FieldCollection fieldColl = availableCT.Fields;
clientContext.Load(fieldColl);
clientContext.ExecuteQuery();
foreach (Field field in fieldColl)
{
//columnFromList is the column taht is to be deleted
if (field.InternalName.Equals(columnFromList))
{
field.DeleteObject();
clientContext.executeQuery();
}
}
Whenever I'm running this code, it throws me an exception:
Additional information: Site columns which are included in content types or on lists cannot be deleted. Please remove all instances of this site column prior to deleting it.
Please suggest me a way to achieve this task programmatically. FYI, when I try to delete it from my Sharepoint website, it gets deleted without any error.
Since the site column is referenced in content type the specified error occurs.
The following examples (implemented as Extension methods) demonstrate how to delete site columns when it is referenced in content type(s):
public static class FieldExtensions
{
/// <summary>
/// Remove column and reference from specific content types
/// </summary>
/// <param name="field"></param>
/// <param name="contentTypeId"></param>
public static void DeleteObject(this Field field,string contentTypeId)
{
var ctx = field.Context as ClientContext;
if (!field.IsPropertyAvailable("Id"))
{
ctx.Load(field, f => f.Id);
ctx.ExecuteQuery();
}
//Firstly, remove site column from content type
var contentType = ctx.Site.RootWeb.ContentTypes.GetById(contentTypeId);
var fieldLinks = contentType.FieldLinks;
var fieldLinkToRemove = fieldLinks.GetById(field.Id);
fieldLinkToRemove.DeleteObject();
contentType.Update(true); //push changes
//Then remove column
field.DeleteObject();
}
/// <summary>
/// Remove column and references from all content types
/// </summary>
/// <param name="field"></param>
/// <param name="includeContentTypes"></param>
public static void DeleteObject(this Field field, bool includeContentTypes)
{
var ctx = field.Context as ClientContext;
if (!field.IsPropertyAvailable("Id"))
{
ctx.Load(field, f => f.Id);
ctx.ExecuteQuery();
}
//Firstly, remove site column link from all content types
ctx.Load(ctx.Site.RootWeb.AvailableContentTypes, cts => cts.Include(ct => ct.FieldLinks));
ctx.ExecuteQuery();
foreach (var ct in ctx.Site.RootWeb.AvailableContentTypes)
{
var containsField = ct.FieldLinks.Any(fl => fl.Id == field.Id);
if (containsField)
{
var fieldLinkToRemove = ct.FieldLinks.GetById(field.Id);
fieldLinkToRemove.DeleteObject();
ct.Update(true); //push changes
}
}
//Then remove site column
field.DeleteObject();
}
}
Usage
Delete site column and references from all content types:
using (var ctx = ClientContext(webUri))
{
var siteFields = ctx.Site.RootWeb.Fields;
var fieldToDel = siteFields.GetByInternalNameOrTitle(fieldName);
fieldToDel.DeleteObject(true);
ctx.ExecuteQuery();
}
Delete site column and reference from content type:
using (var ctx = ClientContext(webUri))
{
//find content type
var result = ctx.LoadQuery(ctx.Site.RootWeb.AvailableContentTypes.Where(ct => ct.Name == "Order Document"));
ctx.ExecuteQuery();
if (result.Any())
{
var ctId = result.First().Id.StringValue;
var siteFields = ctx.Site.RootWeb.Fields;
var fieldToDel = siteFields.GetByInternalNameOrTitle(fieldName);
fieldToDel.DeleteObject(ctId);
ctx.ExecuteQuery();
}
}
I've created a partial class to extend the default spmetal class to handle publishing html fields. As outlined here:
Extending the Object-Relational Mapping
Snippet from public partial class RelatedLinksItem : Item, ICustomMapping:
/// <summary>
/// Read only data is retrieved in this method for each extended SPMetal field
/// Used to Read - CRUD operation performed by SPMetal
/// </summary>
/// <param name="listItem"></param>
[CustomMapping(Columns = new string[] { CONTENT_FIELDtesthtml, CONTENT_FIELDLink })]
public void MapFrom(object listItem)
{
SPListItem item = (SPListItem)listItem;
// link
this.ContentLink = item[CONTENT_FIELDLink] as LinkFieldValue;
// html (does NOT work)
HtmlField html = item[CONTENT_FIELDtesthtml] as HtmlField; // this returns null
// html (does work)
HtmlField html2 = (HtmlField)item.Fields.GetFieldByInternalName(CONTENT_FIELDtesthtml); // this returns object
this.Contenttesthtml = html2;
this.TestHtml = html2.GetFieldValueAsText(item[CONTENT_FIELDtesthtml]); // set property for rendering html
}
Snippet from "webpart":
protected override void CreateChildControls()
{
using (OrganisationalPoliciesDataContext context = new OrganisationalPoliciesDataContext(SPContext.Current.Web.Url))
{
var results = from links in context.RelatedLinks
select links;
foreach (var link in results)
{
// render link
Controls.Add(new LiteralControl(string.Format("<p>Link: {0}</p>", link.ContentLink)));
// render html
Controls.Add(new LiteralControl(string.Format("<p>HTML: {0}</p>", link.TestHtml)));
}
}
}
Two questions:
Why does HtmlField html = item[CONTENT_FIELDtesthtml] as HtmlField; return null, but the item.Fields.GetFieldByInternalName works correctly?
Is there a way to use the GetFieldValueAsText method from within
the webpart or is the approach of storing the value in a custom
property for accessing later acceptable?
You are casting the field value of item[CONTENT_FIELDtesthtml] to the type HtmlField. But HtmlField represents the type of the field and not the type of the field value. Thus HtmlField html will be assigned with null. Check this MSDN page for a reference of all publishing field types and value types.
I am not sure what the field value type of a HtmlField is. Probably just string.
So you should be safe to convert it to string:
string html = Convert.ToString(item[CONTENT_FIELDtesthtml]);
I think storing the value in a property is the way to go. This way you achieve a separation of data layer and presentation layer.
Using Sharepoint 2007 object model, I have been looking for an example in C# to move an item from one document library to another on the same server and saving the version history (i.e. SPListItemVersion objects) and metadata (the folders have the same content types, etc).
I was able to accomplish what I wanted to do with the following code:
/// <summary>
/// Adds item to archive
/// </summary>
/// <param name="item">Item to add</param>
/// <param name="destination">Archive path</param>
/// <param name="destination">web site of archive</param>
/// <returns>Result of arhivation process</returns>
public static string ArchiveItem(SPListItem item, string destination, SPWeb web)
{
// Save main meta information for later use:
var author = item.File.Author;
var modifiedBy = item.File.ModifiedBy;
var modified = item.File.TimeLastModified;
var created = item.File.TimeCreated;
// Get destination filename:
var destinationFile = destination + "/" + item.File.Name;
// Copy the item and set properties:
var coppiedFile = web.GetFolder(destination).Files.Add(
destinationFile,
item.File.OpenBinary(),
author,
modifiedBy,
created,
modified
);
coppiedFile.Item["Created"] = created;
coppiedFile.Item["Modified"] = modified;
// Save changes, UpdateOverwriteVersion causes object to save without saving a new version.
coppiedFile.Item.UpdateOverwriteVersion();
// If moving is enabled, delete original item:
item.Delete();
return coppiedFile.ServerRelativeUrl;
}
I have 3 related tables in my database.
Farm ----> FarmCrops <----- Crops
I'm trying to update the a farm entity with a collection of crops but am running into problems. I've been working on this with no success for hours now so any help would be greatly appreciated.
The error I'm receiving is this:
The object cannot be attached because
it is already in the object context.
An object can only be reattached when
it is in an unchanged state.
My update logic is as follows (my apologies for the large amount of code. I'd just like to be as clear as possible):
bool isNew = false;
Farm farm;
// Insert or update logic.
if (viewModel.Farm.FarmId.Equals(Guid.Empty))
{
farm = new Farm
{
FarmId = Guid.NewGuid(),
RatingSum = 3,
RatingVotes = 1
};
isNew = true;
}
else
{
farm = this.ReadWriteSession
.Single<Farm>(x => x.FarmId == viewModel.Farm.FarmId);
}
// Edit/Add the properties.
farm.Name = viewModel.Farm.Name;
farm.Owner = viewModel.Farm.Owner;
farm.Address = viewModel.Farm.Address;
farm.City = viewModel.Farm.City;
farm.Zip = viewModel.Farm.Zip;
farm.WebAddress = viewModel.Farm.WebAddress;
farm.PhoneNumber = viewModel.Farm.PhoneNumber;
farm.Hostel = viewModel.Farm.Hostel;
farm.Details = viewModel.Farm.Details;
farm.Latitude = viewModel.Farm.Latitude;
farm.Longitude = viewModel.Farm.Longitude;
farm.Weather = viewModel.Farm.Weather;
// Add or update the crops.
string[] cropIds = Request.Form["crop-token-input"].Split(',');
List<Crop> allCrops = this.ReadWriteSession.All<Crop>().ToList();
if (!isNew)
{
// Remove all previous crop/farm relationships.
farm.Crops.Clear();
}
// Loop through and add any crops.
foreach (Crop crop in allCrops)
{
foreach (string id in cropIds)
{
Guid guid = Guid.Parse(id);
if (crop.CropId == guid)
{
farm.Crops.Add(crop);
}
}
}
if (isNew)
{
this.ReadWriteSession.Add<Farm>(farm);
}
else
{
this.ReadWriteSession.Update<Farm>(farm);
}
this.ReadWriteSession.CommitChanges();
My update code within the ReadWriteSession is simple enough (GetSetName<T> just returns the types name from it's PropertyInfo.):
/// <summary>
/// Updates an instance of the specified type.
/// </summary>
/// <param name="item">The instance of the given type to add.</param>
/// <typeparam name="T">The type of entity for which to provide the method.</typeparam>
public void Update<T>(T item) where T : class, new()
{
this.context.AttachTo(this.GetSetName<T>(), item);
this.context.ObjectStateManager.ChangeObjectState(item, EntityState.Modified);
}
You are adding existing Crop objects (from the allCrops list) to the new Farm. When you connect a new entity to an existing one, the new entity automatically gets attached to the context. Therefore you get the error when you try to attach the Farm to the context the second time.
The Add<Farm>(farm) statement in your code is not even necessary to connect the Farm to the context, and if you have an existing Farm that is loaded from the context, it is already attached to the context.
The whole of your if (isNew) statement is unnecessary. Entity framework tracks object state itself, so you don't need to set the modified state.
you don't have to attach the "farm" object at the end, because it's already attached as modified when you change one of its properties. try removing the else statement at the end:
if (isNew)
{
this.ReadWriteSession.Add<Farm>(farm);
}
Hope this helps :)
The problem is in your update method. You can't attach the Farm instance because you have loaded it from the same context so it is already attached and you don't need to call your Update at all because changes to attached objects are tracked automatically.