XML validation error when updating Keyword metadata - tridion

Following on from my earlier question about creating Address Books (many thanks Peter!), I have a small throw-away console application doing just that and working great - but in addition I'm trying to update the metadata of a Keyword with the Item Id of the created Address Book.
Slightly shortened snippet ...
StaticAddressBook ab = new StaticAddressBook();
ab.Title = title;
ab.Key = key;
ab.Save();
// id is a correct Keyword TCM ID
Keyword k = tdse.GetObject(id, EnumOpenMode.OpenModeEdit);
if (k != null)
{
k.MetadataFields["addressbookid"].value[0] = ab.Id.ItemId;
k.Save(true);
}
I keep getting the following error on Save():
XML validation error. Reason: The element 'Metadata' in namespace
'uuid:2065d525-a365-4b45-b68e-bf45f0fba188' has invalid child element
'addressbookid' in namespace
'uuid:2065d525-a365-4b45-b68e-bf45f0fba188'. List of possible elements
expected: 'contact_us_email' in namespace
'uuid:2065d525-a365-4b45-b68e-bf45f0fba188'
But I know the Keyword has the correct Metadata assigned, (thats why I don't bother checking!). Shortened Tridion XML from a current keyword in question:
<tcm:Keyword>
<tcm:Data>
<tcm:MetadataSchemaxlink:type="simple"xlink:title="IP.Location.Metadata" xlink:href="tcm:49-2142-8" />
<tcm:Metadata>
<Metadata xmlns="uuid:2065d525-a365-4b45-b68e-bf45f0fba188">
<email>...</email>
<addressbookid>3</addressbookid>
<contact_us_email>...</contact_us_email>
<request_a_sample_email>...</request_a_sample_email>
<webinar_feedback_email>....</webinar_feedback_email>
</Metadata>
</tcm:Metadata>
<tcm:IsRoot>true</tcm:IsRoot>
</tcm:Data>
</tcm:Keyword>
Have I missed something can Keyword metadata not be updated in this way?
I guess I could look at the Core Service to update Keywords, but it seemed to to make sense to do everything within this application.
UPDATE
Order was key here, strangely!
The following code works:
ItemFields fields = k.MetadataFields;
System.Diagnostics.Debug.WriteLine(fields.Count);
string email = fields[1].value[1];
string contact = fields[3].value[1];
string request = fields[4].value[1];
string webinar = fields[5].value[1];
fields[1].value[1] = email;
fields[2].value[1] = ab.Id.ItemId;
fields[3].value[1] = contact;
fields[4].value[1] = request;
fields[5].value[1] = webinar;
k.Save(true);
Got caught out by the non-0-based index when getting/setting values and had to reassign existing fields back, in order.
Cheers

It seems that the order of the fields has changed in the Schema since that Component was created. At least the Schema expects contact_us_email in the position where you current have addressbookid.
There may be other changes, so I'd verify the order of fields in the Schema and make sure the Component(s) match, before you run your tool.

Related

When I call an AIF service from C# to create a SO, my XML doesn't look like I would expect

I'm experimenting with AIF to create a basic SO from C# following this general guide from Microsoft. I have two questions.
The only information I'm passing is this:
// Create instances of the entities that are used in the service and
// set the needed fields on those entities.
AxdEntity_SalesTable salesTable = new AxdEntity_SalesTable();
salesTable.CurrencyCode = "USD";
salesTable.CustAccount = "100003";
salesTable.DeliveryDate = Convert.ToDateTime("1/14/2016");
salesTable.Payment = "Net30";
salesTable.PurchOrderFormNum = "PO";
AxdEntity_SalesLine salesLine = new AxdEntity_SalesLine();
salesLine.ItemId = "44417";
salesLine.SalesQty = 3;
salesLine.SalesUnit = "ea";
Why is it that when I examine the XML, it looks like it's passing tons of extra fields:
What does this error mean? EInvoiceAccountCode appears to be a base field on SalesTable, and I tried Tools>Application Integration Framework>Update document service to update the SalesSalesOrder service.
Invalid document schema. The following error was returned: The element 'SalesTable' in namespace 'http://schemas.microsoft.com/dynamics/2008/01/documents/SalesOrder' has invalid child element 'EInvoiceAccountCode' in namespace 'http://schemas.microsoft.com/dynamics/2008/01/documents/SalesOrder'. List of possible elements expected: 'DlvTerm' in namespace 'http://schemas.microsoft.com/dynamics/2008/01/documents/SalesOrder'.
1) Everything is flagged as Nillable by Ax (for several reasons). Nillable VS MinOccurs 0
2) You are passing an element "EInvoiceAccountCode" in your request. But it's expecting an "DlvTerm" element at that location.

Keyword has incorrect values in custom meta

I have a category with keywords which in their tern have metadata schema. That schema consist of two fields and each of them is category. Very simple structure, but during publishing it resolves those metadata keyword fields into wrong tcm uris instead of title of the keyword, like the following:
2) Content of the deployer package
<tcmc:Topic rdf:about="tcm:10-11325-1024">
<rdfs:label>Analytics and optimization</rdfs:label>
<rdfs:comment>Analytics and optimization</rdfs:comment>
<tcmt:key>Analytics and optimization</tcmt:key>
<tcmt:isAbstract>false</tcmt:isAbstract>
<tcmt:isRoot>true</tcmt:isRoot>
<tcmt:metadata rdf:parseType="Literal">
<Metadata xmlns="uuid:a30b06d3-b6c5-4c2e-a53b-2b88771370ed">
<Divisions xlink:title="cma" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="tcm:0-17737-1024">cma</Divisions>
<InterestProfile xlink:title="CMAAnalytics" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="tcm:0-11175-1024">CMAAnalytics</InterestProfile>
</Metadata>
</tcmt:metadata>
</tcmc:Topic>
3) In code where I query Tridion it returns these uris:
TaxonomyFactory taxonomyFactory = new TaxonomyFactory();
TKeyword taxonomy = taxonomyFactory.GetTaxonomyKeywords(“tcm_of_the_category”);
if (taxonomy != null && taxonomy.KeywordChildren != null)
{
foreach (var item in taxonomy.KeywordChildren) //keyword metadata contains tcm uri with zero instead of title
{
Keyword keywordChildren = item as Keyword;
if (keywordChildren != null)
{
. . .
}
}
}
Does anyone have any ideas what might cause such an issue?
At a glance, my guess is that the internal template used to transform the categories is reading the metadata field data directly from the DB (or near enough in the BL layer) and not applying any blueprinting rules to it (likely for performance).
If you look at TCM Uris in content, when stored in the database, they all use 0 as their publication ID, and this ID is modified at "read" time.
Your call: You can call this a defect, ask Tridion to fix it, and it will degrade the performance of publishing a category, or you can deal with it in the delivery side - you know the publication Uri is 0, and you know you need to replace it with the current publication ID if you intend to use it for any purpose.
EDIT
So I went back and did some quick hacking. Indeed you can't load the keyword's content because, according to Tridion, the "Value" of field "Divisions" is the keyword URI. No way around that.
Quick way around this: load the keyword in question:
TaxonomyFactory tf = new TaxonomyFactory();
Keyword taxonomy = tf.GetTaxonomyKeywords("tcm:5-369-512");
if(taxonomy != null && taxonomy.KeywordChildren != null)
{
foreach (Keyword item in taxonomy.KeywordChildren)
{
NameValuePair key = (NameValuePair) item.KeywordMeta.NameValues["Field1"];
string correctUri = key.Value.ToString().Replace("tcm:0-", "tcm:5-");
Keyword theOtherKeyword = tf.GetTaxonomyKeyword(correctUri);
string title = theOtherKeyword.KeywordName;
}
}
Now... you probably want to be a bit smarter than me on that creative publication ID rewrite :)
You can see the field as a Component Link, you link to a specific Keyword item (object). Therefor you primarily get the URI, and I don't think that it resolves automatically to the Value property.
So the next step would be to obtain the Keyword object using the URI, and possibly construct the URI to include the right publication context.

SDL Tridion GetListKeywords using Anquilla Framework

I'm writing a GUI extension and using the Anquilla framework to get a list of Keywords within a Category. I'm obtaining an XML document for the list of keywords then working with that document within my extension.
My problem is that the returned XML doesn't contain the Keyword's 'Description' value. I have the Title and Key etc.
My original code looks like this:
var category = $models.getItem("CATEGORYTCMID:);
var list = category.getListKeywords();
list.getXml();
A typical node returned is this:
<tcm:Item ID="tcm:4-1749-1024"
Type="1024" Title="rate_one" Lock="0" IsRoot="true"
Modified="2012-12-17T23:01:59" FromPub="010 Schema"
Key="rate_one_value" IsAbstract="false"
CategoryTitle="TagSelector"
CategoryID="tcm:4-469-512" Icon="T1024L0P0"
Allow="268560384" Deny="96" IsNew="false"
Managed="1024"/></tcm:ListKeywords>
So I've tried using a Filter to give me additional column information:
var filter = new Tridion.ContentManager.ListFilter();
filter.columns = Tridion.Constants.ColumnFilter.EXTENDED;
var list = category.getListKeywords(filter);
Unfortunately this only gives the additional XML attributes:
IsShared="true" IsLocalized="false"
I'd really like the description value to be part of this XML without having to create a Keyword object from the XML. Is such a thing possible?
cough any ideas? cough
I'm afraid you'll have to load the Keyword itself to get the Description.
It's not used in any lists, so it's not returned in the XML.
You could always create a List Extender to add this information to the list, but try to be smart about it since this extender will execute everytime a GetList is called.
Won't save you from having to open every keyword in the list, but you'll be doing it server-side (with Core Service/NetTcp for instance) which will probably be easier and faster than opening each keyword with Anguilla.
In this instance I only need the one keyword, so I simply get it from the CMS. Getting an object in Anguilla is a bit weird, here's the code:
In your main code area:
var selectedKy = $models.getItem("TcmUriOfKeywordHere");
if (selectedKy.isLoaded()) {
p.selectedKy = selectedKy;
this.onselectedKyLoaded();
} else {
$evt.addEventHandler(selectedKy, "load", this.onselectedKyLoaded);
selectedKy.load();
}
It's worth noting how I store the keyword in the properties of the item, so I can obtain it in the onselectedKyLoaded function
The function called once the item is loaded
ContentBloom.ExampleGuiExtension.prototype.onselectedKyLoaded = function (event) {
var p = this.properties;
var selectedDescription = p.selectedKy.getDescription();
// do what you need to do with the description :)
};
I resolved this, thanks to the answer here: https://stackoverflow.com/a/12805939/1221032 - Cheers Nuno :)

How to override VBScript GetObject method in .NET

I am having below code in VBScript
' Retrieve the keyword category for page section names
Set SectionCat = TDSE.GetObject(WebdavToUri(getPublicationWebDav(WEBDAV_SECTION_CAT)), 1)
' Retrieve the localized section keyword
Set SectionKeyword = SectionCat.GetKeywordByTitle(meta)
' Open the English translated section keyword
Set SectionKeyword = TDSE.GetObject(SectionKeyword.Id, 1, WEBDAV_UKEN_PUB)
SectionName = SectionKeyword.Title
Where WEBDAV_UKEN_PUB is the WebDavPath, now in VBScript GetObject method we have got option to pass three parameters 1) Item.ID, 2) TDSDefines.OpenModeEditWithFallback and 3) WebDavPath from where to make the object.
Now I want to write same logic in 2009 .Net templating, below is the sample code, I am trying to write but not able to get rid of VBScript Object.
Category cat = engine.GetSession().GetObject(WebdavToUri(getPublicationWebDav(Constants.WEBDAV_SECTION_CAT,package,engine), engine)) as Category;
if (cat != null)
{
//_log.Info("Category" + cat.Title);
Keyword keyword = cat.GetKeywordByTitle(meta);
//_log.Info("keyword 1" + keyword.Title);
keyword = engine.GetObject(Constants.WEBDAV_UKEN_PUB) as Keyword;
//_log.Info("keyword 2 " + keyword.Title);
if (keyword != null)
{
sectionName = keyword.Title;
}
keyword = null;
I am able to create Category object, however when I am trying to make Keyword object its getting failed and giving object reference error.
Do we have any class or method which work same like VBScript GetObject which will make the Object from the passed webdavpath or can somebody can give sample code on this.
I think your problem is here:
keyword = engine.GetObject(Constants.WEBDAV_UKEN_PUB) as Keyword;
You are using the WEBDav URL of a publication, and then attempting a dynamic cast to Keyword. You can't cast a Publication to a Keyword, so the cast fails and your keyword variable is assigned null.
Using dynamic casts in this way is an easy way to fool yourself. The "As" keyword (C# keyword not Tridion keyword) should be used when you don't know at compile time what type you expect. If you know that you expect an item of type Keyword, then you should write:
keyword = (Keyword)engine.GetObject(Constants.WEBDAV_UKEN_PUB);
This way - when the cast fails, you'll get an exception that identifies the problem correctly.
In TOM.NET we cannot get an object and specify which pub to read it from, we need to modify the TcmUri to be in context.
So:
Repository context = (Repository)session.GetObject(WEBDAV_UKEN_PUB);
TcmUri keywordInContext = new TcmUri(keyword.Id.ItemId, keyword.Id.ItemType, context.Id.ItemId);
Keyword keyword = (Keyword)session.GetObject(keywordInContext);

Entity Framework: Insists on adding new entity in many-to-many instead of re-using existing FK

I have got a many to many relationship, briefly
Cases -----< CaseSubjectRelationships >------ CaseSubjects
More fully:
Cases(ID, CaseTypeID, .......)
CaseSubjects(ID, DisplayName, CRMSPIN)
CaseSubjectsRelationships(CaseID, SubjectID, PrimarySubject, RelationToCase, ...)
In my many-to-many link table are additional properties relating to the subject's association with the specific case - such as, start date, end date, free-text relationship to case (observer, creator, etc)
An Entity Framework data model has been created - ASP.NET version 4.0
I have a WCF service with a method called CreateNewCase which accepts as its parameter a Case object (an entity created by the Entity Framework) - its job is to save the case into the database.
The WCF service is invoked by a third party tool. Here is the SOAP sent:
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<CreateNewCase xmlns="http://tempuri.org/">
<c xmlns:a="http://schemas.datacontract.org/2004/07/CAMSModel">
<a:CaseSubjectsRelationships>
<a:CaseSubjectsRelationship>
<a:CaseSubject>
<a:CRMSPIN>601</a:CRMSPIN>
<a:DisplayName>Fred Flintstone</a:DisplayName>
</a:CaseSubject>
<a:PrimarySubject>true</a:PrimarySubject>
<a:RelationToCase>Interested</a:RelationToCase>
<a:StartDate>2011-07-12T00:00:00</a:StartDate>
</a:CaseSubjectsRelationship>
<a:CaseSubjectsRelationship>
<a:CaseSubject>
<a:CRMSPIN>602</a:CRMSPIN>
<a:DisplayName>Barney Rubble</a:DisplayName>
</a:CaseSubject>
<a:RelationToCase>Observer</a:RelationToCase>
<a:StartDate>2011-07-12T00:00:00</a:StartDate>
</a:CaseSubjectsRelationship>
</a:CaseSubjectsRelationships>
<a:CaseType>
<a:Identifier>Change of Occupier</a:Identifier>
</a:CaseType>
<a:Description>Case description</a:Description>
<a:Priority>5</a:Priority>
<a:QueueIdentifier>Queue One</a:QueueIdentifier>
<a:Title>Case title</a:Title>
</c>
</CreateNewCase>
</s:Body>
</s:Envelope>
The WCF engine deserializes this into a Case entity for me correctly and when I look in the debugger everything is set up properly.
What I want to do, is only create a new CaseSubject if there is not already an entry in the database with that CRMSPIN specified (CRMSPIN is a reference number from a central customer database)
So, in the below example, I want to see if I already have an entry in CaseSubjects for somebody with CRMSPIN 601 and if I do, I don't want to create another (duplicate) entry but instead make the new case link to the existing subject (although a new row will need, obviously, need creating in CaseSubjectsRelationships with the specific 'additional' information such as relationship etc)
Here is the .NET code I have tried to do this.
Public Class CamsService
Implements ICamsService
Public Function CreateNewCase(c As CAMSModel.Case) As String Implements ICamsService.CreateNewCase
Using ctx As New CAMSEntities
' Find the case type '
Dim ct = ctx.CaseTypes.SingleOrDefault(Function(x) x.Identifier.ToUpper = c.CaseType.Identifier.ToUpper)
' Give an error if no such case type '
If ct Is Nothing Then
Throw New CaseTypeInvalidException(String.Format("The case type {0} is not valid.", c.CaseType.Identifier.ToString))
End If
' Set the case type based on that found in database: '
c.CaseType = ct
For Each csr In c.CaseSubjectsRelationships
Dim spin As String = csr.CaseSubject.CRMSPIN
Dim s As CaseSubject = ctx.CaseSubjects.SingleOrDefault(Function(x) x.CRMSPIN = spin)
If Not s Is Nothing Then
' The subject has been found based on CRMSPIN so set the subject in the relationship '
csr.CaseSubject = s
End If
Next
c.CreationChannel = "Web service"
c.CreationDate = Now.Date
' Save it '
ctx.AddToCases(c)
ctx.SaveChanges()
End Using
' Return the case reference '
Return c.ID.ToString
End Function
End Class
As you can see, instead the For Each loop, I try to get a subject based on the CRMSPIN and if I get something, then I update the "CaseSubject" entity. (I have also tried csr.SubjectID = s.ID instead of setting the whole entity and also I have tried setting them both!).
However, even when putting a breakpoint on the ctx.SaveChanges() line and looking at how the subjects are set up and seeing in the debugger that it looks fine, it is always creating a new row in the CaseSubjects table.
I can see in principle this should work - you'll see I've done exactly the same thing for Case Type - I have picked the identifier sent in the XML, found the entity with that identifier via the context, then changed the case's .CaseType to the entity I found. When it saves, it works perfectly and as-expected and with no duplicated rows.
I'm just having trouble trying to apply the same theory to one side of a many-to-many relationship.
Here are some (hopefully relevant) extracts from the .edmx
<EntitySet Name="Cases" EntityType="CAMSModel.Store.Cases" store:Type="Tables" Schema="dbo" />
<EntitySet Name="CaseSubjects" EntityType="CAMSModel.Store.CaseSubjects" store:Type="Tables" Schema="dbo" />
<EntitySet Name="CaseSubjectsRelationships" EntityType="CAMSModel.Store.CaseSubjectsRelationships" store:Type="Tables" Schema="dbo" />
<AssociationSet Name="FK_CaseSubjectsRelationships_Cases" Association="CAMSModel.Store.FK_CaseSubjectsRelationships_Cases">
<End Role="Cases" EntitySet="Cases" />
<End Role="CaseSubjectsRelationships" EntitySet="CaseSubjectsRelationships" />
</AssociationSet>
<AssociationSet Name="FK_CaseSubjectsRelationships_CaseSubjects" Association="CAMSModel.Store.FK_CaseSubjectsRelationships_CaseSubjects">
<End Role="CaseSubjects" EntitySet="CaseSubjects" />
<End Role="CaseSubjectsRelationships" EntitySet="CaseSubjectsRelationships" />
</AssociationSet>
EDIT: The property setters for the CaseSubject property of the CaseSubjectsRelationships object:
/// <summary>
/// No Metadata Documentation available.
/// </summary>
<XmlIgnoreAttribute()>
<SoapIgnoreAttribute()>
<DataMemberAttribute()>
<EdmRelationshipNavigationPropertyAttribute("CAMSModel", "FK_CaseSubjectsRelationships_CaseSubjects", "CaseSubject")>
Public Property CaseSubject() As CaseSubject
Get
Return CType(Me, IEntityWithRelationships).RelationshipManager.GetRelatedReference(Of CaseSubject)("CAMSModel.FK_CaseSubjectsRelationships_CaseSubjects", "CaseSubject").Value
End Get
Set
CType(Me, IEntityWithRelationships).RelationshipManager.GetRelatedReference(Of CaseSubject)("CAMSModel.FK_CaseSubjectsRelationships_CaseSubjects", "CaseSubject").Value = value
End Set
End Property
You didn't specify what context model are you working with, so I'll assume you're using the default (ie. you don't have some explicit .tt files to generate your entities).
So, basically, this is what I think is happening.
In your code, when you fetch something from context:
Dim ct = ctx.CaseTypes.SingleOrDefault(Function(x) x.Identifier.ToUpper = c.CaseType.Identifier.ToUpper)
this ct is in context. The method argument that you deserialized from service (the c) is not in context. You can regard the context as the "object tracking and fetching" entity, that makes sure that everything attached to it can know about any changes, if it's new, deleted etc.
So, when you get to the part:
' Set the case type based on that found in database: '
c.CaseType = ct
at the moment you assign something that's attached to something not attached, the unattached object will get pulled into context as well - there can't be "partially" attached entities - if it's attached, everything it references has to be attached as well. So, this is the moment where the c gets "dragged" into the context (implicitly). When it enters the context, it will get marked as "new" since it doesn't know anything about it yet (it has no knowledge of it, no change tracking info...).
So, now that everything about that object c is in context, when you query the context for this:
Dim s As CaseSubject = ctx.CaseSubjects.SingleOrDefault(Function(x) x.CRMSPIN = spin)
it will figure that indeed there is an object with that CRMSPIN and it's already attached - "hey, no need to go to database, I already have this!" (trying to be smart and avoid a db hit), and it will return your own object.
Finally, when you save everything, it will be saved, but your attached c and all of it's child objects that are marked as 'new' will be inserted instead of updated.
The easiest fix would be to first query everything you need from context, and only then start assigning it to properties of your object. Also, take a look at UpdateCurrentValues, it may also be helpful...
OK: So the resolution to this was a combination of what #veljkoz said in his answer (which was very useful to help me out to reach the final resolution, but on its own was not the full resolution)
By moving the For Each loop to the first thing done before anything else (As hinted by #veljkoz), that got rid of the Collection was modified, enumeration may not continue error I was getting when I set csr.CaseSubject = Nothing.
It also turned out to be important to not attach entities (e.g. not to set csr.CaseSubject to an entity but only to Nothing) but instead to use the .SubjectID property. A combination of all the above led me to the following code, which works perfectly and doesn't try to insert duplicate rows.
+1 to #veljkoz for the assist but also note that the resolution includes setting the entity reference to Nothing and using the ID property.
Full, working code:
Public Function CreateNewCase(c As CAMSModel.Case) As String Implements ICamsService.CreateNewCase
Using ctx As New CAMSEntities
' Subjects first, otherwise when you try to set csr.CaseSubject = Nothing you get an exception '
For Each csr In c.CaseSubjectsRelationships
Dim spin As String = csr.CaseSubject.CRMSPIN
Dim s As CaseSubject = ctx.CaseSubjects.SingleOrDefault(Function(x) x.CRMSPIN = spin)
If Not s Is Nothing Then
' The subject has been found based on CRMSPIN so set the subject in the relationship '
csr.CaseSubject = Nothing
csr.SubjectID = s.ID
End If
Next
' Find the case type '
Dim ct = ctx.CaseTypes.SingleOrDefault(Function(x) x.Identifier.ToUpper = c.CaseType.Identifier.ToUpper)
' Give an error if no such case type '
If ct Is Nothing Then
Throw New CaseTypeInvalidException(String.Format("The case type {0} is not valid.", c.CaseType.Identifier.ToString))
End If
' Set the case type based on that found in database: '
c.CaseType = ct
c.CreationChannel = "Web service"
c.CreationDate = Now.Date
' Save it '
ctx.AddToCases(c)
ctx.SaveChanges()
End Using
' Return the case reference '
Return c.ID.ToString
End Function

Resources