.NET - WCF RIA Services code generatie naar je hand zetten
- Datum:
- Auteur: Stefan Cruysberghs
- Deze pagina is enkel in het Engels beschikbaar
In March of this year SP1 of WCF RIA Services was released together with SP1 of Visual Studio 2010. Soon this was followed by a new version of the WCF RIA Services Toolkit. Microsoft has changed a few things in SP1 which makes it possible for you to implement you own code generation classes that can manipulate the client code generation of RIA Services entities, contexts, ... In my opinion this is a great new feature which is very powerful when you are building your own Silverlight framework.
There is not much information about these features available on the internet yet. The most interesting source is the blog of Varun Puranik, one of the Microsoft developers of WCF RIA Services.
So in this article I will describe how the code generation in RIA services SP1 works and I will demonstrate some scenarios and ideas were customizing the generated code is a nice technique to extend your own framework. I will try to focus on the code generation aspects so views and viewmodels, dynamic loading of modules, ... will not be handled in these examples.
- WCF RIA Services code generation
- Setup of demo application
- Installation of Toolkit
- T4 Code generation classes
- Adding interfaces to the generated entities
- Adding attributes to the generated entities
- Using derived data annotation attributes
- Adding XML comments to the generated entities
- Using derived classes for entities, domain contexts and the web context
- Implementing your own logic in the derived classes
WCF RIA Services code generation
I'm using RIA Services since the first betas because it is a powerful framework which generates a lot of Silverlight client code for you. When you have implemented entities, DTO's and domain services on the server side an MSBuild task will automatically generate client proxies each time you build your project. RIA Services can also copy validation or other logic from the service side to the client. For each DomainService a client side DomainContext will be created and these DomainContexts will load your entities and DTO's and they support change tracking and submitting changes.
Setup of demo application
I created a simple Silverlight Navigation Application with an ASP.NET project . At server side I created a folder called Domain with an Entity Framework Model that includes some entities of the Northwind database.
In the folder Services a RIA DomainService class was implemented for the Product entity. This ProductService looks like this:
using System.Data;
using System.Linq;
using System.ServiceModel.DomainServices.EntityFramework;
using System.ServiceModel.DomainServices.Hosting;
using System.ServiceModel.DomainServices.Server;
using ScipBe.Demo.RiaCodeGen.Web.Domain;
namespace ScipBe.Demo.RiaCodeGen.Web.Services
{
[EnableClientAccess()]
public class ProductService : LinqToEntitiesDomainService<NorthwindEntities>
{
[Query(IsDefault = true)]
public IQueryable<Product> GetProducts()
{
var products = ObjectContext.Products;
return products;
}
public IQueryable<Product> GetProductsByName(string searchText)
{
var products = ObjectContext.Products.Where(p => p.ProductName.Contains(searchText));
return products;
}
public void InsertProduct(Product product)
{
if ((product.EntityState != EntityState.Detached))
{
ObjectContext.ObjectStateManager.ChangeObjectState(product, EntityState.Added);
}
else
{
ObjectContext.Products.AddObject(product);
}
}
public void UpdateProduct(Product currentProduct)
{
ObjectContext.Products.AttachAsModified(currentProduct,
ChangeSet.GetOriginal(currentProduct));
}
public void DeleteProduct(Product product)
{
if ((product.EntityState != EntityState.Detached))
{
ObjectContext.ObjectStateManager.ChangeObjectState(product, EntityState.Deleted);
}
else
{
ObjectContext.Products.Attach(product);
ObjectContext.Products.DeleteObject(product);
}
}
}
}
The Silverlight project has a WCF RIA Services link to the server side ASP.NET project which hosts the DomainServices.
When you build the solution the code geneneration will be executed. In the hidden folder Generated_Code in the Silverlight project you will find following default generated client code.
Now you can easily access the your product data in Silverlight by calling Load methods of the ProductContext class:
var context = new ProductContext();
context.Load(context.GetProductsQuery());
context.Load(
context.GetProductsByNameQuery("ton"),
LoadBehavior.MergeIntoCurrent,
loadOperation =>
{
var products = context.Products;
var loadedEntities = loadOperation.Entities;
},
null);
For more information about RIA Services you can read the documentation at MSDN.
Installation of Toolkit
Now we can start modifying this project. First of all we have to install the WCF RIA Services Toolkit (April 2011). You can download the installation package from the Microsoft website.
Another and easier way is to open the new package manager NuGet which is available after installing Visual Studio SP1. Choose Get Library Package Reference and look for RiaServices in the online gallery. Then install the RIAServices.T4 library.
Since the April update of the Toolkit you do not need to modify the Silverlight project file anymore. Before this release it was required to add the <RiaClientCodeGeneratorName> tag in the project file.
T4 Code generation classes
The T4 Code Generator classes are included in the Microsoft.ServiceModel.DomainServices.Tools and Microsoft.ServiceModel. DomainServices.Tools.TextTemplate assemblies. The T4 Code Generator consists of 2 parts:
The CSharpClientCodeGenerator class and the DomainServiceClientCodeGenerator attribute are the main components that hook into the extensibility features of the WCF RIA Services code generation process.
Secondly there are 5 different code generators:
- CSharpDomainContextGenerator
- CSharpEntityGenerator
- CSharpComplexObjectGenerator
- CSharpEnumGenerator
- CSharpWebContextGenerator
Each of the 5 code generators have protected virtual methods which you can override to change the code generation of a specific part of a class (class declaration, constructor, property, ...). And finally there is a TransformText method which contains all the generated code.
So in my server project I added a Generators folder with a MyCodeGenerator class derived from CSharpClientCodeGenerator and decorated with the DomainServiceClientCodeGenerator attribute.
using Microsoft.ServiceModel.DomainServices.Tools;
using Microsoft.ServiceModel.DomainServices.Tools.TextTemplate;
using Microsoft.ServiceModel.DomainServices.Tools.TextTemplate.CSharpGenerators;
namespace ScipBe.Demo.RiaCodeGen.Web.Generators
{
[DomainServiceClientCodeGenerator("MyCodeGenerator", "C#")]
public class MyCodeGenerator : CSharpClientCodeGenerator
{
protected override EntityGenerator EntityGenerator
{
get { return new MyEntityGenerator(); }
}
protected override DomainContextGenerator DomainContextGenerator
{
get { return new MyDomainContextGenerator(); }
}
protected override WebContextGenerator WebContextGenerator
{
get { return new MyWebContextGenerator(); }
}
protected override ComplexObjectGenerator ComplexObjectGenerator
{
get { return base.ComplexObjectGenerator; }
}
protected override EnumGenerator EnumGenerator
{
get { return base.EnumGenerator; }
}
}
}
I also created 3 other generator classes for the entity, domain context and web context.
using Microsoft.ServiceModel.DomainServices.Tools.TextTemplate.CSharpGenerators;
using ScipBe.Demo.RiaCodeGen.Web.Domain;
namespace ScipBe.Demo.RiaCodeGen.Web.Generators
{
public class MyEntityGenerator : CSharpEntityGenerator
{
public override string TransformText()
{
return base.TransformText();
}
protected override void GenerateClassDeclaration()
{
base.GenerateClassDeclaration();
}
protected override void GenerateProperty(PropertyDescriptor propertyDescriptor)
{
base.GenerateProperty(propertyDescriptor);
}
}
}
namespace ScipBe.Demo.RiaCodeGen.Web.Generators
{
public class MyDomainContextGenerator : CSharpDomainContextGenerator
{
public override string TransformText()
{
return base.TransformText();
}
protected override void GenerateClassDeclaration()
{
base.GenerateClassDeclaration();
}
}
}
namespace ScipBe.Demo.RiaCodeGen.Web.Generators
{
public class MyWebContextGenerator : CSharpWebContextGenerator
{
public override string TransformText()
{
return base.TransformText();
}
protected override void GenerateClassDeclaration()
{
base.GenerateClassDeclaration();
}
}
}
If you build your project again you will see that the generated code (ScipBe.Demo.RiaCodeGen.Web.g.cs) will be different from the default code generation. The functionality is the same but the order of the classes differs and comment headers are not generated.
Let us start manipulating the default code generation behaviour.
Adding interfaces to the generated entities
Wouldn't it be handy if you could use the same interfaces for your entities at server and client side? With just a few lines of code we can accomplish this. First I created an IId interface with an int Id property. I renamed the key properties in my Northwind EDM and I added the IId interface in the partial class of each entity.
namespace ScipBe.Demo.RiaCodeGen.Web.Domain
{
public interface IId
{
int Id { get; set; }
}
}
namespace ScipBe.Demo.RiaCodeGen.Web.Domain { public partial class Product : IId { } }
Because the interface is also needed on client side I created a shared link to this file in the Domain folder in my Silverlight project.
In the MyEntityGenerator class I implemented the GenerateClassDeclaration method and there I added a using to the namespace of the IId interface. Finally in the TransformText method I check if the entity is derived from IId and then this interface is added in the generated code by replacing strings.
namespace ScipBe.Demo.RiaCodeGen.Web.Generators
{
public class MyEntityGenerator : CSharpEntityGenerator
{
protected override void GenerateClassDeclaration()
{
WriteLine("using ScipBe.Demo.RiaCodeGen.Web.Domain;");
base.GenerateClassDeclaration();
}
public override string TransformText()
{
var generatedCode = base.TransformText();
if (typeof(IId).IsAssignableFrom(Type))
{
generatedCode = generatedCode
.Replace(
"System.ServiceModel.DomainServices.Client.Entity",
"System.ServiceModel.DomainServices.Client.Entity, IId");
}
return generatedCode;
}
}
}
The generated code will look like this:
Adding attributes to the generated entities
The same approach can be used to decorate the generated client entities with attributes. I like to use the DebuggerVisualizer attribute which makes it easier to inspect collection of entities in the Local and Watch windows of Visual Studio. So on server side I decorated the Product class with the DebuggerDisplay attribute.
using System.Diagnostics;
namespace ScipBe.Demo.RiaCodeGen.Web.Domain
{
[DebuggerDisplay("{ProductName}")]
public partial class Product : IId
{
}
}
In the TransformText method in the MyEntityGenerator class I added some code to check if the class is decorated with the attribute and then I replace some of the generated code. Don't forget to add the using of System.Diagnostics.
using Microsoft.ServiceModel.DomainServices.Tools.TextTemplate.CSharpGenerators;
using ScipBe.Demo.RiaCodeGen.Web.Domain;
namespace ScipBe.Demo.RiaCodeGen.Web.Generators
{
public class MyEntityGenerator : CSharpEntityGenerator
{
protected override void GenerateClassDeclaration()
{
WriteLine("using System.Diagnostics;");
base.GenerateClassDeclaration();
}
public override string TransformText()
{
var generatedCode = base.TransformText();
var debuggerDisplayAttributes = Type.GetCustomAttributes(
typeof(DebuggerDisplayAttribute), true)
.Cast<DebuggerDisplayAttribute>().FirstOrDefault();
if (debuggerDisplayAttributes != null)
{
var displayName = debuggerDisplayAttributes.Value;
generatedCode = generatedCode
.Replace("public sealed partial class",
"[DebuggerDisplay(\"" + displayName
+ "\")]\r\n public sealed partial class");
}
return generatedCode;
}
}
}
When debugging the Silverlight project you will be able to see the names of the products in Visual Studio Locals, Watches and tooltip windows.
Using derived data annotation attributes
The Entity Framework and RIA Services support several data annotation attributes like Required, StringLength, RegularExpression, Range, ... You can add these attributes in your own metadata classes but RIA Services also adds them automatically for constraints which are defined on the database. By modifying the generated code you can implement your own derived attributes.
using System.ComponentModel.DataAnnotations;
namespace ScipBe.Demo.RiaCodeGen.Framework
{
public class MyRequiredAttribute : RequiredAttribute
{
}
}
public override string TransformText()
{
var generatedCode = base.TransformText();
generatedCode = generatedCode
.Replace("[System.ComponentModel.DataAnnotations.RequiredAttribute()]", "[MyRequired]");
return generatedCode;
}
protected override void GenerateClassDeclaration()
{
WriteLine("using ScipBe.Demo.RiaCodeGen.Framework;");
base.GenerateClassDeclaration();
}
Adding XML comments to the generated entities
I also found a nice example of an implementation of the T4 Code Generators at the blog of Yavor Georgiev who is a Program Manager at Microsoft. He describes how you can copy the XML comment of the classes and properties of EDM entities.
I tried to improve his sources a little bit and moved the logic to a XmlCommentsProvider class.
using System;
using System.Linq;
using System.Xml.Linq;
using System.Text;
using System.IO;
using System.ComponentModel;
using System.Diagnostics;
namespace ScipBe.Demo.RiaCodeGen.Web.Generators
{
public class XmlCommentsProvider
{
private XElement comments;
public XmlCommentsProvider(Type classType, string path)
{
var assemblyXmlFileName = path.Replace("dll", "xml");
comments = XElement.Load(new FileStream(assemblyXmlFileName, FileMode.Open));
}
private XElement FindCommentElementByName(string name)
{
return comments
.Descendants("member")
.Where(x => String.Compare(x.Attributes("name").FirstOrDefault().
Value.ToString(),
name, true) == 0)
.FirstOrDefault();
}
private string PrintInnerXml(XElement element)
{
string[] lines = element.Nodes()
.Aggregate("", (b, node) => b += node.ToString())
.Split(new string[] { Environment.NewLine },
StringSplitOptions.RemoveEmptyEntries);
var sb = new StringBuilder();
foreach (string line in lines)
{
sb.AppendLine(line.Trim().Insert(0, "///"));
}
return sb.ToString().Trim();
}
public string GetClassComments(Type classType)
{
var element = FindCommentElementByName("T:" + classType.FullName);
if (element != null)
{
return PrintInnerXml(element);
}
return null;
}
public string GetPropertyComments(PropertyDescriptor propertyDescriptor)
{
var element = FindCommentElementByName("P:"
+ propertyDescriptor.ComponentType.FullName
+ "." + propertyDescriptor.Name);
if (element != null)
{
return PrintInnerXml(element);
}
return null;
}
}
}
Now I can call the XmlCommentsProvider. GetClassComments method in the GenerateClassDeclaration method and XmlCommentsProvider. GetPropertyComments in the GenerateProperty method.
namespace ScipBe.Demo.RiaCodeGen.Web.Generators
{
public class MyEntityGenerator : CSharpEntityGenerator
{
XmlCommentsProvider xmlCommentsProvider;
protected override void GenerateClassDeclaration()
{
// Add XML documentation for class
xmlCommentsProvider =new XmlCommentsProvider(this.Type,
this.ClientCodeGenerator.Options.ClientProjectPath);
WriteLine(xmlCommentsProvider.GetClassComments(this.Type));
base.GenerateClassDeclaration();
}
protected override void GenerateProperty(PropertyDescriptor propertyDescriptor)
{
// Add XML documentation for property
if (xmlCommentsProvider != null)
{
var comments = xmlCommentsProvider.GetPropertyComments(propertyDescriptor);
if (!string.IsNullOrEmpty(comments))
{
WriteLine(comments);
}
}
base.GenerateProperty(propertyDescriptor);
}
}
}
If you add comments to the entity
then the final generated code will look like this:
Using derived classes for entities, domain contexts and the web context
I always prefer to create derived classes from the .NET classes in my framework so that I have the freedom to add new or modify default behaviour. On server side I always implement my own DomainService class. Now we can also implement derived classes on the client for Entity, DomainContext and WebContext. So I added 3 classes MyEntity, MyDomainContext and MyWebContext in a Framework folder in the Silverlight project.
using System.Runtime.Serialization;
using System.ServiceModel.DomainServices.Client;
namespace ScipBe.Demo.RiaCodeGen.Framework
{
[DataContract]
public class MyEntity : Entity
{
}
}
using System;
using System.Diagnostics;
using System.ServiceModel;
using System.ServiceModel.DomainServices.Client;
namespace ScipBe.Demo.RiaCodeGen.Framework
{
public abstract class MyDomainContext : DomainContext
{
}
}
using System.ServiceModel.DomainServices.Client.ApplicationServices;
namespace ScipBe.Demo.RiaCodeGen.Framework
{
public class MyWebContext : WebContextBase
{
}
}
The next step is very easy and you just have to replace some text in the TransformText method. This is how my MyEntityGenerator class will look with all the previous examples implemented.
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using Microsoft.ServiceModel.DomainServices.Tools.TextTemplate.CSharpGenerators;
using ScipBe.Demo.RiaCodeGen.Web.Domain;
namespace ScipBe.Demo.RiaCodeGen.Web.Generators
{
public class MyEntityGenerator : CSharpEntityGenerator
{
XmlCommentsProvider xmlCommentsProvider;
public override string TransformText()
{
var myEntity = "MyEntity";
if (typeof(IId).IsAssignableFrom(Type))
{
myEntity += ", IId";
}
var generatedCode = base.TransformText();
var debuggerDisplayAttributes = Type.GetCustomAttributes(
typeof(DebuggerDisplayAttribute), true)
.Cast<DebuggerDisplayAttribute>().FirstOrDefault();
if (debuggerDisplayAttributes != null)
{
var displayName = debuggerDisplayAttributes.Value;
generatedCode = generatedCode
.Replace("public sealed partial class",
"[DebuggerDisplay(\"" + displayName
+ "\")]\r\n public sealed partial class");
}
generatedCode = generatedCode
.Replace("System.ServiceModel.DomainServices.Client.Entity", myEntity);
generatedCode = generatedCode
.Replace("[System.ComponentModel.DataAnnotations.RequiredAttribute()]",
"[MyRequired]");
return generatedCode;
}
protected override void GenerateClassDeclaration()
{
WriteLine("using ScipBe.Demo.RiaCodeGen.Framework;");
WriteLine("using ScipBe.Demo.RiaCodeGen.Web.Domain;");
WriteLine("using System.Diagnostics;");
// Add XML documentation for class
xmlCommentsProvider = new XmlCommentsProvider(this.Type);
WriteLine(xmlCommentsProvider.GetClassComments(this.Type));
base.GenerateClassDeclaration();
}
protected override void GenerateProperty(PropertyDescriptor propertyDescriptor)
{
// Add XML documentation for property
if (xmlCommentsProvider != null)
{
var comments = xmlCommentsProvider.GetPropertyComments(propertyDescriptor);
if (!string.IsNullOrEmpty(comments))
{
WriteLine(comments);
}
}
base.GenerateProperty(propertyDescriptor);
}
}
}
The MyDomainContextGenerator and MyWebContextGenerator classes are implemented in the same way.
using Microsoft.ServiceModel.DomainServices.Tools.TextTemplate.CSharpGenerators;
namespace ScipBe.Demo.RiaCodeGen.Web.Generators
{
public class MyDomainContextGenerator : CSharpDomainContextGenerator
{
public override string TransformText()
{
return base.TransformText()
.Replace(
"System.ServiceModel.DomainServices.Client.DomainContext",
"MyDomainContext");
}
protected override void GenerateClassDeclaration()
{
WriteLine("using ScipBe.Demo.RiaCodeGen.Framework;");
base.GenerateClassDeclaration();
}
}
}
using Microsoft.ServiceModel.DomainServices.Tools.TextTemplate.CSharpGenerators;
namespace ScipBe.Demo.RiaCodeGen.Web.Generators
{
public class MyWebContextGenerator : CSharpWebContextGenerator
{
public override string TransformText()
{
return base.TransformText()
.Replace(
"System.ServiceModel.DomainServices.Client.ApplicationServices.WebContextBase",
"MyWebContext");
}
protected override void GenerateClassDeclaration()
{
WriteLine("using ScipBe.Demo.RiaCodeGen.Framework;");
base.GenerateClassDeclaration();
}
}
}
The generated code will have following changes:
Implementing your own logic in the derived classes
Now you can start to add your own logic in these framework classes. For example you could modify the endpoint timeouts in the MyDomainContext class. It is a lot easier to debug a Silverlight RIA Services application when the timeouts are increased.
namespace ScipBe.Demo.RiaCodeGen.Framework
{
public abstract class MyDomainContext : DomainContext
{
public MyDomainContext(DomainClient domainClient) : base(domainClient)
{
var channelFactory = (ChannelFactory)DomainClient.GetType().GetProperty("ChannelFactory")
.GetValue(DomainClient, null);
var timeout = new TimeSpan(0, 5, 00);
if (Debugger.IsAttached)
{
timeout = new TimeSpan(0, 15, 00);
}
var endpointBinding = channelFactory.Endpoint.Binding;
endpointBinding.OpenTimeout = timeout;
endpointBinding.CloseTimeout = timeout;
endpointBinding.SendTimeout = timeout;
}
}
}
You could also create your own overloaded Load methods.
namespace ScipBe.Demo.RiaCodeGen.Framework
{
public abstract class MyDomainContext : DomainContext
{
public LoadOperation Load(EntityQuery query, Action<LoadOperation> callback)
{
return Load(query, LoadBehavior.RefreshCurrent, callback, null);
}
}
}
I don't know if you ever faced the problem of logging out with RIA Services while domain contexts or still loading or submitting data. You will get exceptions. Therefore it would be handy of you could check if domain contexts somewhere in your viewmodels are still processing. In the singleton MyWebContext class I added a 2 static methods to increase and decrease a counter and in the Load method of my MyDomainContext I call this increase method and in the callback action the counter is decreased. Of course you should do the same in the SubmitChanges and the two InvokeOperation methods.
using System;
using System.Diagnostics;
using System.ServiceModel;
using System.ServiceModel.DomainServices.Client;
namespace ScipBe.Demo.RiaCodeGen.Framework
{
public abstract class MyDomainContext : DomainContext
{
public MyDomainContext(DomainClient domainClient) : base(domainClient)
{
var channelFactory = (ChannelFactory)DomainClient.GetType()
.GetProperty("ChannelFactory")
.GetValue(DomainClient, null);
var timeout = new TimeSpan(0, 5, 00);
if (Debugger.IsAttached)
{
timeout = new TimeSpan(0, 15, 00);
}
var endpointBinding = channelFactory.Endpoint.Binding;
endpointBinding.OpenTimeout = timeout;
endpointBinding.CloseTimeout = timeout;
endpointBinding.SendTimeout = timeout;
}
public LoadOperation Load(EntityQuery query, Action<LoadOperation> callback)
{
return Load(query, LoadBehavior.RefreshCurrent, callback, null);
}
public override LoadOperation Load(EntityQuery query, LoadBehavior loadBehavior,
Action<LoadOperation> callback, object userState)
{
Action<LoadOperation> newCallback =
loadOperation =>
{
MyWebContext.DecreaseBusyCounter();
if (callback != null)
{
callback.Invoke(loadOperation);
}
};
MyWebContext.DecreaseBusyCounter();
return base.Load(query, loadBehavior, newCallback, userState);
}
public override SubmitOperation SubmitChanges(Action<SubmitOperation> callback,
object userState)
{
Action<SubmitOperation> newCallback =
submitOperation =>
{
MyWebContext.DecreaseBusyCounter();
if (callback != null)
{
callback.Invoke(submitOperation);
}
};
MyWebContext.DecreaseBusyCounter();
return base.SubmitChanges(newCallback, userState);
}
public override InvokeOperation<TValue> InvokeOperation<TValue>(
string operationName, Type returnType,
System.Collections.Generic.IDictionary<string, object> parameters, bool hasSideEffects,
Action<InvokeOperation<TValue>> callback, object userState)
{
Action<InvokeOperation<TValue>> newCallback =
invokeOperation =>
{
MyWebContext.DecreaseBusyCounter();
if (callback != null)
{
callback.Invoke(invokeOperation);
}
};
MyWebContext.DecreaseBusyCounter();
return base.InvokeOperation<TValue>(operationName, returnType, parameters,
hasSideEffects, newCallback, userState);
}
public override InvokeOperation InvokeOperation(string operationName, Type returnType,
System.Collections.Generic.IDictionary<string, object> parameters, bool hasSideEffects,
Action<InvokeOperation> callback, object userState)
{
Action<InvokeOperation> newCallback =
invokeOperation =>
{
MyWebContext.DecreaseBusyCounter();
if (callback != null)
{
callback.Invoke(invokeOperation);
}
};
MyWebContext.DecreaseBusyCounter();
return base.InvokeOperation(operationName, returnType, parameters,
hasSideEffects, newCallback, userState);
}
}
}
using System.ServiceModel.DomainServices.Client.ApplicationServices;
namespace ScipBe.Demo.RiaCodeGen.Framework
{
public class MyWebContext : WebContextBase
{
private static object lockObject = new object();
private static int busyCounter;
public static void IncreaseBusyCounter()
{
lock (lockObject)
{
busyCounter++;
}
}
public static void DecreaseBusyCounter()
{
lock (lockObject)
{
busyCounter--;
}
}
public bool IsBusy
{
get { return busyCounter > 0; }
}
}
}
Now you can easily check if domain contexts are busy loading or submitting data.
if (!WebContext.Current.IsBusy)
{
WebContext.Current.Authentication.Logout();
}
I hope that you like my T4 code generation examples for RIA Services and that you can take advantage of it. It is a very powerful feature and I'm quite sure these features will be extended in future versions. If you have any other ideas, remarks or suggestions, please let me know.
Downloaden
- Inhoud: Demo applicatie WCF RIA Services code generatie naar je hand zetten.
- Versie: (2011-06-12)
- Grootte: 195.9 Kb
- Auteur: Stefan Cruysberghs