Silverlight + SharePoint - add to web part gallery

This article covers how to go about packaging a webpart for the Silverlight XAP file and deploy it to Web Part Gallery.

In the previous article I described the steps to create a SharePoint project and deploy the XAP file via a site feature.

Silverlight + SharePoint 2010 - package XAP file in a sandbox WSP Solution

But the user still needs to manually:

  1. Find the XAP file URL
  2. Manually insert a Microsoft out of box Silverlight Web Part
  3. Paste the XAP URL
  4. Save the Silverlight Web Part (may have to configure InitParams too).

Let's simplify this a little.

Go back to the <Elements> Module, and lets add a webpart

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Module Name="SilverlightCamera">
    <File Path="SilverlightCamera\SilverlightCamera.xap" Url="Style Library/SilverlightCamera.xap" />
    <File Path="SilverlightCamera\SilverlightCamera.webpart" Url="_catalogs/wp/SilverlightCamera.webpart" />
  </Module>
</Elements>

 

The webpart file itself is very simple: go to Web Parts Gallery, find and export Microsoft's Silverlight.webpart

<?xml version="1.0" encoding="utf-8"?>
<webParts>
  <webPart xmlns="http://schemas.microsoft.com/WebPart/v3">
    <metaData>
      <type name=
"Microsoft.SharePoint.WebPartPages.SilverlightWebPart, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
      <importErrorMessage>Cannot import this Web Part.</importErrorMessage>
    </metaData>
    <data>
      <properties>
        <property name="Title" type="string">Silverlight Camera Web Part</property>
        <property name="Description" type="string">A web part to display a Silverlight Camera.</property>
        <property name="Url" type="string">~sitecollection/Style Library/SilverlightCamera.xap</property>
        <property name="Height" type="int">300</property>
      </properties>
    </data>
  </webPart>
</webParts>

 

You may have noticed that the Url is a SiteCollection relative (~sitecollection/) path, so that no matter whether it's a root site collection or a managed path, we want the user's experience to be perfect.

Unfortunately - that syntax doesn't work, not without one extra hack via the Feature Event Receiver.

Feature Activated Event Receiver

 

Right click on the feature and add a new event receiver.  VS.NET will generate the CS file.

clip_image002

Figure: right click on the feature (.feature) and add an event receiver. 

 

Waldek has a magical event handler code that fixes this problem

http://blog.mastykarz.nl/inconvenient-content-query-web-part-server-relative-urls/

So I'll just post my bit of code here

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
    // one day when I meet Waldek in person I will buy him beer
    // http://blog.mastykarz.nl/inconvenient-content-query-web-part-server-relative-urls/
    SPSite site = properties.Feature.Parent as SPSite;
    if (site != null)
    {
        SPList webPartsGallery = site.GetCatalog(SPListTemplateType.WebPartCatalog);
        SPListItemCollection allWebParts = webPartsGallery.Items;
        SPListItem webPart = (from SPListItem wp in allWebParts
                              where wp.File.Name == "SilverlightCamera.webpart"
                              select wp).SingleOrDefault();
        if (webPart != null)
        {
            string siteCollectionUrl = site.ServerRelativeUrl;
            if (!siteCollectionUrl.EndsWith("/"))
            {
                siteCollectionUrl += "/";
            }
            string fileContents = Encoding.UTF8.GetString(webPart.File.OpenBinary());
            fileContents = fileContents.Replace("~sitecollection/", siteCollectionUrl);
            webPart.File.SaveBinary(Encoding.UTF8.GetBytes(fileContents));
        }
    }
}

 

During Feature Activated - find the webpart and fix the ~sitecollection marker with the real site collection URL.

 

Deploy and activate the feature again.

clip_image002[5]

 

Success!  Our .webpart now appearing in the web part gallery.

Silverlight + SharePoint 2010 - package XAP file in a sandbox WSP Solution

This is a long series of blog posts on developing, debugging and deploying Silverlight and SharePoint solutions.

/blog/2010/6/18/develop-and-deploy-silverlight-sharepoint-2010-solutions.html
/blog/2010/6/22/develop-and-deploy-silverlight-sharepoint-2010-solutions-par.html
/blog/2010/6/28/develop-and-deploy-silverlight-sharepoint-2010-solutions-par.html
/blog/2010/10/18/silverlight-sharepoint-2010-did-you-just-deploy-customizatio.html

 

Assuming by this point, you have built a XAP file from a Silverlight project, and can deploy it manually to SharePoint by uploading to a SharePoint document library and link up Microsoft's Silverlight web part to "play" your XAP file.

Now let's see how we can build a WSP package.

Create a SharePoint Project in your solution

 

clip_image002

Figure: Add a new Empty SharePoint Project

 

clip_image002[5]\

Figure: Provide a debug site, and choose Sandboxed solution

 

clip_image002[7]

Figure: Add a module to this project - you should give it a good name

 

clip_image002[9]

Figure: Select Properties of this module…

 

The following is very special.  If you blink you will miss this!

 

From the Module's properties, find Project Output References and open this dialog.
Then from the dialog

  • select "ElementFile" for deployment type
  • select the project output of your Silverlight project

This step ensures that the Silverlight project output (XAP file), is automatically included as an element file in your SharePoint module. 

Magical!  But very well hidden UI.  Most people don't know it's there!

clip_image002[11]

Figure: Add the Project Output to this module.

 

Open up the module.xml file and check:

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Module Name="Module1">
    <File Path="Module1\SLSPConf.xap" Url="Style Library/SLSPConf.xap" />
  </Module>
</Elements>

I set the destination URL to be Style Library.  You can put it elsewhere but you'll need somewhere where people can actually have read-access.

 

Deploy to Solution Gallery

 

clip_image002[13]
Figure: Deploy the Silverlight solution to Solutions Gallery - you can activate the sandbox solution here.

 

clip_image002[15]

Figure: Activate the site feature

 

VS.NET debugging tip

 

Go to the properties for the SharePoint project.  Select the SharePoint tab, and scroll right down.

  • Tick "Enable Silverlight debugging"
  • Untick "Auto-retract after debugging" - this one makes VS.NET deploy and activate when F5.  But as soon as you stop debugging VS.NET will retract the solution - so your Silverlight stops working!

 

clip_image002[17]

Figure: VS.NET SharePoint project settings.

Silverlight - sharing a common class between Silverlight and .NET

Because Silverlight is compiled against a separate set of Silverlight runtime, we can not reference or share a common library between a Silverlight project and a normal .NET project.

One very common and simple workaround is then to create a common project for .NET, and a common project for Silverlight, and then add the files as existing links from the .NET project to the Silverlight project.  This ensures that the same files are shared by the two sides and we have our matching class definitions.

 

With RIA, there is a new way.

image

In the .NET project, name your extra files with xxx.shared.cs

Compile your .NET project.  This triggers the RIA toolkit to run and generate some files for us:

image

 

So you no longer need to share a file via external link.

Silverlight - the magic of Silverlight RIA Toolkit

 

I checked in some code.  Moments later, my colleague asked me:

"did you check in the service reference for this ServiceReference.DispatchKing.Web.Services.RunboardDuplexService"

I pondered, and answered:

"no need - it was generated for me"

In deed - my Silverlight project started to do something magical for me.  It was creating GeneratedWcfClientCode\ServiceReference.cs

Because this was generated like RIA services, it was not included in the project, and thus - if you didn't have the right tools installed, it appears that your colleague has forgotten to check in files!

 

image

 

Memory Lane

In the old days before RIA services, we rolled out WCF Service and added our own Service Reference via the Silverlight project.  This generated all sorts of service reference proxy classes.

The biggest downside, is that each time we updated the service, we had to regenerate the service reference, otherwise they'd become out of sync and Silverlight will crash and burn.  (or in the case where it didn't crash… we got really scared).

 

In Silverlight 3 we got RIA and the DomainService, having Silverlight project linked to a Web Project meant that Visual Studio automatically started generating all the RIA/DomainService code, as well as the data objects (or entity) required, and making sure everything's available and synchronized between the Web (service) and Silverlight (client).  There was much rejoicing.

What's this magic?

What made this case really special then, is that I'm not using only Domain Services.

image

 

Hmm it doesn't work for my colleague.  We quickly compared out installed Programs and Features, and found out why:

image

 

You can grab this via Web platform installer, or directly from:

http://www.microsoft.com/downloads/en/details.aspx?FamilyID=7b43bab5-a8ff-40ed-9c84-11abb9cda559&displaylang=en

The RIA Services Toolkit enables the following 5 features:

 

1. LinqToSql DomainService
2. Soap endpoint - This enables you to expose a soap endpoint for your DomainService
3. JSON endpoint - This enables you to expose a JSON endpoint for your DomainService
4. ASP.net DomainDataSource - This control will enable you to create an ASP.net application that can talk to your DomainService
5. WCF client proxy auto generation/updating for WCF Core Service – This enables you to get up-to-date WCF proxy and configuration each time you build your solution, when you add Silverlight-enable WCF service in your Silverlight application.

 

The magic, is number 5.

Silverlight - RadScheduler and required field data validation

In my current project I'm having the pleasure of working with the Silverlight RadScheduler control.  While you may read and feel it is a very bloated control, I argue otherwise, it is highly extensible for such a complex control.  I really think the Telerik guys did a very good job.

My particular scenario is interesting.  I need to:

  1. Extend the appointment dialog to support addresses
  2. The addresses aren't always required (so I can't just use RequiredAttribute)
  3. When fields are required, I'd like the validation to kick off and prevent me from saving that appointment

Here we go:

Updating the RadScheduler template:

This is pretty simple - find the EditAppointmentTemplate

  <ControlTemplate x:Key="EditAppointmentTemplate" TargetType="telerik:AppointmentDialogWindow">
  1. Customize the control template, we added additional fields
  2. NotifyOnValidationError and ValidatesOnException are important.
..snip

          <input:ValidationSummary x:Name="ValidationSummary" />

          <TextBlock Text="Suburb" Style="{StaticResource FormElementTextBlockStyle}"/>
          <TextBox Text="{Binding Suburb, Mode=TwoWay, NotifyOnValidationError=True, 
ValidatesOnExceptions=True}" />

Extend the Appointment class:

  1. Create a ValidationEnabled property - sometimes you need to "not validate"
  2. Create a ValidateProperty method - this takes a property name and triggers all validate attributes on that property - Validator.ValidateProperty is the method that will trigger all kinds of fun validation exceptions for us.
  3. Set the appropriate validation attributes on the property e.g. Suburb - I'm using a custom validation property that I've created based on the RequiredAttribute
  4. In the setter, call ValidateProperty.
    public class JobAppointment : Appointment
    {
        private string _suburb;

        public ObservableCollection<ValidationError> Errors { get; set; }

        public bool ValidationEnabled
        {
            get;
            set;
        }

        private void ValidateProperty(string propertyName, object value)
        {
            if (ValidationEnabled)
            {
                var context = new ValidationContext(this, null, null);
                context.MemberName = propertyName;

                Validator.ValidateProperty(value, context);
            }
        }

        [ValidationRequired]
        public string Suburb
        {
            get
            {
                return _suburb;
            }
            set
            {
                ValidateProperty("Suburb", value);

                if (_suburb != value)
                {
                    _suburb = value;
                    OnPropertyChanged("Suburb");
                }
            }
        }

Add our own Validation attributes

  1. This Required attribute checks against our settings to see if "Suburb" happens to be a required field.  If not, then skip the validation and just return success.
 public class ValidationRequiredAttribute : RequiredAttribute
    {
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            if (SettingsLifeTimeService.Current != null )
            {
                if (SettingsLifeTimeService.Current.RequiredFields.Contains(validationContext.MemberName))
                {
                    // RequiredAttribute validation
                    return base.IsValid(value, validationContext);
                }
            }
            return ValidationResult.Success;
        }
    }

 

Stop validation on Scheduler's Appointment_Saving event

  1. In the AppointmentSaving event, grab all the children framework elements from the appointment dialog window
  2. Find their binding expression (e.g., for TextBox I want the binding expression for Textbox.Text)
  3. If exists, I want it to push the values back into the datasource (our customized Appointment class) - this triggers the property setter (which triggers our ValidateProperty, which triggers the custom Required attribute)
  4. Finally, check the validation summary to see if we have any binding errors, if we do, cancel the save.
void _scheduler_AppointmentSaving(object sender, AppointmentSavingEventArgs e)
        {
            JobAppointment jobApp = e.Appointment as JobAppointment;
            AppointmentDialogWindow window = e.OriginalSource as AppointmentDialogWindow;
            var children = window.ChildrenOfType<FrameworkElement>();

            if (children != null)
            {
                foreach (var element in children)
                {
                    BindingExpression binding = null;
                    if (element is TextBox)
                    {
                        binding = element.GetBindingExpression(TextBox.TextProperty);
                    }

                    if (binding != null)
                    {
                        // force control to update databound VM.  This triggers the validation.
                        binding.UpdateSource();
                    }
                }
            }

            ValidationSummary summary = window.FindChildByType<ValidationSummary>();

            if (summary != null)
            {
                if (summary.HasErrors)
                {
                    e.Cancel = true;
                    return;
                }
            }
        }

Finished!

 

image