Compartilhar via


Using LINQ-to-SharePoint with Choice Fields (Ricky Kirkham)

If you like to use the LINQ to SharePoint provider to perform your SharePoint data queries in server-side code, sooner or later you will encounter a certain tension between the strongly typed world of LINQ queries and the decentralized nature of SharePoint. To enable LINQ querying, source code must include an object-relational mapping that turns lists and content types into classes and turns fields into strongly-typed properties of the content type classes. This code is generated by the SPMetal tool. But website owners can add new lists after your solution has been deployed. The new lists are not accounted for in the object-relational mapping. Similarly, new fields can be added to sites and lists. These are also not accounted for in the object-relational mapping.

Of course, if your LINQ queries and data changes only reference the lists, content types, and fields that were present when the object-relational mapping was generated at design-time, the presence of new entities does not cause a problem. But sometimes a mismatch between the design-time object-relational mapping and the runtime reality of the website content that is being queried can cause problems for your LINQ work. Many of these kinds of problems can be solved by extending the object-relational mapping using the techniques described in Extending the Object-Relational Mapping. In this post, I want to describe a problem, and suggest a solution, that is not mentioned in the latter article.

When SPMetal generates code for a Choice field, it will invent an enum type to represent the possible values of the field. For example, the following code is generated for the standard Priority field on the standard Tasks list:

public enum Priority : int
{
None = 0,

    Invalid = 1,

    [Microsoft.SharePoint.Linq.ChoiceAttribute(Value = "(1) High")]
_1High = 2,

    [Microsoft.SharePoint.Linq.ChoiceAttribute(Value = "(2) Normal")]
_2Normal = 4,

     [Microsoft.SharePoint.Linq.ChoiceAttribute(Value = "(3) Low")]
_3Low = 8,
}

But what happens if the list owner adds “(4) Trivial” as an additional possible choice to the definition of the Priority column? For task items that have the new value in their Priority field, a LINQ query will return “Invalid” rather than “(4) Trivial” as the value of the field.

The essence of the problem here is that data schemas that can be changed by end users are inherently weakly typed, but the object-relational mapping is imposing a strong-typing straitjacket onto the schema. To solve the problem, we need to loosen that straitjacket; that is, we need to weaken the type of the Choice field from enum to string; but not weaken it too much.

An Easy Solution, But Not Always Applicable

If all you are going to do is query the data, there is a easy solution provided by the configurability of the SPMetal tool. You can change the tool’s default behavior by using a parameters.xml file that is referenced in the command line call to SPMetal. The details of how to do this are in SPMetal and Overriding SPMetal Defaults by Using a Parameters XML File. In this case, you tell SPMetal to treat the Priority field as a string rather than an enum with a parameters.xml file such as the following.

<?xml version="1.0" encoding="utf-8"?>
<Web xmlns="https://schemas.microsoft.com/SharePoint/2009/spmetal">
<ContentType Name="Task" Class="Task">
<Column Name="Priority" Member="Priority" Type="String" />
</ContentType>
<ExcludeContentType Name="Summary Task"/>
</Web>

Side note: You will notice that I’m also excluding the Summary Task content type from the object-relational mapping, this is to avoid having to do some of the other chores in my solution twice because the Summary Task content type also has the Priority field. 

With this parameters.xml file in use, SPMetal will create the following signature for the Priority property of the Task class:

public String Priority

Instead of the following signature which it would have created by default.

public System.Nullable<Priority> Priority

Side note: The Priority field is also in the standard team website as a site column, so SPMetal will still generate the enum to serve as the type of that site column, but it will not use the enum to be the type of the Priority column in the Task content type.

Now your LINQ queries will return “(4) Trivial” or any other choice that is added to the Priority column definition, rather than “Invalid”.

When You Want to Write to the Choice Field Too

But what if you are going to write data with the LINQ-to-SharePoint provider as well as query it? (You can do that as explained in How to: Write to Content Databases Using LINQ to SharePoint.) Now that you’ve weakened the type of the Priority field, code can write any string to the field, even one that is not included in the column definition’s list of possible choices. You could promise yourself that you will always write code that checks the list of possible choices before it writes to a Choice field, but you do not want to have to re-write that code every time you write to a Choice field with the LINQ to SharePoint provider. One way to make sure that the needed validation executes every time is to put the validating code right in the setter of the property that represents the Choice field. The following explains a way that you can do that.

The first thing you need to do is move the code that declares and implements the property out of the file that SPMetal generates. If you do not do this your customization of the setter in that property would be overwritten whenever SPMetal regenerates the code, and you are almost never in a situation in which you know for certain that you will never have to regenerate the code again.

Fortunately, the classes that SPMetal generates are marked with the partial keyword. That means that you can re-declare the class in another code file and add additional member definitions to the class in the new file.

To continue with the example of the Tasks list and the Priority column,  take these steps:

1. Point SPMetal at site that has a Tasks list and generate your code.

2. Add a new class code file to your Visual Studio project. Delete the stub class declaration that is created automatically and delete the namespace declaration, too.

Side note: The code file generated by SPMetal does not declare a namespace, which means the default namespace of the Visual Studio project is assumed. If you have a partial class defined in two code files and one does not put the class inside an explicit namespace declaration, then the other code file cannot put the class inside an explicit namespace declaration either, not even an explicit declaration of the default namespace. This seems to be a quirk of the way the compiler deals with partial classes. Hence, you have to delete that explicit namespace declaration that Visual Studio automatically included in the new class code file.

3. Add using statements for Microsoft.SharePoint and Microsoft.SharePoint.Linq.

4. Copy the signature of the Task class declaration from the generated code to your new file and add opening and closing braces . (Do not copy the attributes above the class declaration.)

public partial class Task : Item {

}

5. Copy the backing field for the Priority property in the generated code and paste it into the Task class in your new file. If you have previously applied the Easy Solution (described above) to this project, then the backing field declaration looks like this:

private String _priority;

Otherwise, it looks like this:

private System.Nullable<Priority> _priority;

6. Move the Priority class declaration from the generated file to the new file in the same way. In this case you do copy the attribute on the declaration.

[Microsoft.SharePoint.Linq.ColumnAttribute(Name = "Priority", Storage = "_priority", FieldType = "Choice")]
public System.Nullable<Priority> Priority
{
get {

return this._priority;
}

    set {

        if ((value != this._priority)) {

            this.OnPropertyChanging("Priority", this._priority);
this._priority = value;
this.OnPropertyChanged("Priority");
}
}
}

Again, the type of the property will be String, instead of System.Nullable<Priority>, if you’ve already applied the Easy Solution to this project.

7. If you have not previously applied the Easy Solution to this project, you now need to change “System.Nullable<Priority>” in both places where it appears in your new file to “String”.

8. You cannot declare the same property and backing field in both files. Unlike the classes, properties cannot be marked “partial”. So, you need to ensure that they are removed from the generated code file and not regenerated on subsequent runs of SPMetal. To accomplish that, create a parameters.xml file with the following content.

<?xml version="1.0" encoding="utf-8"?>
<Web AccessModifier="Internal" xmlns="https://schemas.microsoft.com/SharePoint/2009/spmetal">
<ContentType Name="Task" Class="Task">
<ExcludeColumn Name="Priority" />
</ContentType>
<ExcludeContentType Name="Summary Task"/>
</Web>

9. Rerun SPMetal with a command line that references your new parameters.xml file. The new generated code file will no longer contain the declarations of the Priority property or its backing field.

Now you need to add your validation logic to the setter of the Choice field class. To continue the example, take these steps.

1. Add the following code to the Priority property setter just above the “if’ statement:

using (SPSite siteCollection = new SPSite(----- ????? -----))
{
    using (SPWeb website = siteCollection.OpenWeb(---- ????? -----))
{
SPList taskList = website.GetList(this.Path);
SPFieldChoice priorityField = taskList.Fields["Priority"] as SPFieldChoice;

        if (!priorityField.Choices.Contains(value))
{
throw new ArgumentOutOfRangeException("value", String.Format("'{0}' is not a possible value for the Choice field 'Priority' in the 'Tasks' list.", value));
}
}
}

Working from the bottom up, note the following about this code. If the value that calling code is passing is not in the Choices collection of the SPFieldChoice object, then an exception is thrown. The reference to the field object is obtained through the Fields collection of the SPList object, and a reference to the latter object, in turn, is obtained through the GetList method of the SPWeb object and the Path property of the Task object.

Before any of this can be done, however, the code needs to get references to the SPWeb and SPSite objects. If the code is running where there is an HTTP Context, and thus a non-null SPContext object, your code may be able to get current SPSite and SPWeb objects from the SPContext object. But let’s try to write code that can be used even in a console application or other scenario in which there is no HTTP Context.

2. To get a reference to the SPWeb object, we need a site-collection-relative URL for the website. You can construct this URL by starting with the Path property of the Task object and trimming off from the end of it the website-relative URL of the list. So add the following lines above the “using (SPWeb …” line:

String webRelativeListURL = "/Lists/Tasks";
String siteRelativeWebURL = this.Path.Remove(this.Path.Length - webRelativeListURL.Length);

3. Insert siteRelativeWebURL as the parameter to the OpenWeb method.

You need the absolute URL of the parent site collection to get a reference to the SPSite object. This is harder. The Task class’s Path property does not include the protocol or domain part of the site collection URL. Nor is this information in any other member of the Task class that SPMetal generates. What you need to do is create a new member of the Task class that can hold this information and then initialize that member when the Task object is created. To continue the example, take the following steps:

1. Add the following declaration of an internal field to the Task class in your code file (not the generated code file).

internal String parentDataContextWebURL;

You’ll see in a moment why we are giving it that name. For now just note that it is going to hold the URL that is needed by the SPSite constructor.

2. Replace the parameter “----- ????? -----” in the SPSite constructor with the name of your new field: parentDataContextWebURL.

The content type class has a constructor that is generated by SPMetal and this is a part of SPMetal behavior that you cannot turn off or change. So you cannot customize this constructor to initialize your new internal field (because  you customization would be overwritten the next time the code is regenerated). Moreover, creating another constructor in your own partial definition of the class would serve no purpose. This is because your LINQ calling code does not directly construct the list object and the items within it. Rather these entities are created by the GetList method of the DataContext object. And that method, in turn, calls the generated constructor – the one you cannot modify.

You will notice in the generated code that the content type class constructor does call the partial method OnCreated, so you could give this method an implementation in your own partial class definition. Unfortunately, this method takes no parameters (nor does the constructor that calls it) so there is no way to pass it the data it would need to initialize your new field. (Again, if there’s an HTTP context, we could perhaps get the needed information from that, but we are trying to write Choice field validation code that can be run even when there is no such context.)

What you need to do is override the GetList method of the DataContext object so that it initializes the new field on each list object before it returns the list. You can do this because the DataContext-derived class that SPMetal generates is also marked partial. To continue the example, take the following steps:

1. Near the very top of the generated code file is the declaration of the DataContext-derived class. Copy that declaration to the new code file you created earlier and then add the opening and closing braces. The name of the class is determined by what you used as the value of the /code parameter on the command line call of SPMetal. 

public partial class LinqChoiceFieldExperimentsDataContext : Microsoft.SharePoint.Linq.DataContext {

}

2. Add the following override of the DataContext.GetList method to the class.

public override EntityList<T> GetList<T>(string listName)
{
EntityList<T> list = base.GetList<T>(listName);

    if (typeof(T).Name == "Task")
{
foreach (T t in list)
{
Task task = t as Task;
task.parentDataContextWebURL = this.Web;
}
}
return list;
}

Note that the method is passing the DataContext object’s Web property (which holds the object’s absolute URL) to the internal field of each Task object in the list. This field, in turn, is used by your customized setter to create an SPSite object. This URL, by the way, might be the URL of a subsite of the site collection. That does not cause any problems. The SPSite constructor is smart enough to know that what you really want is the parent SPSite of whatever entity is located at the URL in the parameter that is passed to the constructor.

That does it. Calling code, such as the following, will be blocked from writing invalid values to the Choice field.

using (LinqChoiceFieldExperimentsDataContext lCFDC = new LinqChoiceFieldDataContext(https://Contoso/Marketing/))
{
EntityList<Task> tasks = lCFDC.GetList<Task>("Tasks");
tasks.First().Priority = "(4) Trivial";
lCFDC.SubmitChanges();
}

Of course, in its present version, this code only works for the Priority column of the Tasks list.  Consider generalizing it to deal with all Choice fields on all lists. As my college math textbooks used to say, “this is left as an exercise for the reader”.

Comments

  • Anonymous
    April 23, 2012
    I had no idea this limitation existed. It seems like such a gross over site on the part of Microsoft. Thanks for the solution!