Umbraco :: Catching or Extending User Login - asp.net

I am trying to catch the login event of Umbraco Users (login in the CMS).
I have tried to extend from MembersMembershipProvider and override the ValidateUser method. I also changed the web.config to use my class.
When i put a breakpoint in this overrited method it doesnt stop and logsin the user as usual.
public class CustomUmbracoMembershipProvider : Umbraco.Web.Security.Providers.UsersMembershipProvider
{
public override bool ValidateUser(string username, string password)
{
return base.ValidateUser(username, password);
}
}
Thanks in advance.

As Jannik said, Umbraco uses ASP.NET Identity in the latest couple of versions, so the MembershipProvider is not used to validate and authenticate the user anymore.
So after a couple hours of research, i found a workaround solution:
1º - Create a custom UserManager, and override the method CheckPasswordAsync:
public class CustomBackOfficeUserManager : BackOfficeUserManager
{
public CustomBackOfficeUserManager(
IUserStore<BackOfficeIdentityUser, int> store,
IdentityFactoryOptions<BackOfficeUserManager> options,
MembershipProviderBase membershipProvider) :
base(store, options, membershipProvider)
{
}
/// <summary>
/// Returns true if the password is valid for the user
/// </summary>
/// <param name="user"/><param name="password"/>
/// <returns/>
public override Task<bool> CheckPasswordAsync(BackOfficeIdentityUser user, string password)
{
//Your implementation
var result = base.CheckPasswordAsync(user, password).Result;
return Task.FromResult(result);
}
}
2º - Then in your owin startup, you can use this block of code for your ConfigureUserManagerForUmbracoBackOffice:
var appCtx = ApplicationContext;
app.ConfigureUserManagerForUmbracoBackOffice<BackOfficeUserManager, BackOfficeIdentityUser>(
appCtx,
(options, context) =>
{
var membershipProvider = MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider();
var store = new BackOfficeUserStore(
appCtx.Services.UserService,
appCtx.Services.ExternalLoginService,
membershipProvider);
return new CustomBackOfficeUserManager(store, options, membershipProvider);
});
This solution ill keep using the umbraco UserStore and the base methods of UserManager.

I have a similar situation. This worked for me in Umbraco 7.6.5. I know it's not elegant. It's only a workaround:
public class MyCustomEvents: ApplicationEventHandler
{
protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
base.ApplicationStarted(umbracoApplication, applicationContext);
UserService.SavedUser += UserServiceSaved;
}
private void UserServiceSaved(IUserService sender, SaveEventArgs<IUser> e)
{
foreach(IUser user in e.SavedEntities)
{
if (!user.IsNewEntity()) //Is not creating a new user
{
IUser alreadyLoggedUser = UmbracoContext.Current.Security.CurrentUser;
if (alreadyLoggedUser == null) //Is not a user management via backoffice
{
if (user.FailedPasswordAttempts == 0) //Is a successful login?
{
DateTime justNow = DateTime.Now.AddSeconds(-5);
if (user.LastLoginDate.CompareTo(justNow) >= 0) //Logged in just now?
{
//Do your stuff
}
}
}
}
}
}
}

As the Umbraco backend uses ASP.NET Identity in the latest couple of versions, it may also be relevant to ask which exact version you are using.
I think ASP.NET Identity has an OnLoggedIn event you could try: https://msdn.microsoft.com/en-us/library/system.web.ui.webcontrols.login.onloggedin.aspx
But I'm not 100% sure the link is current.

Related

Using Unity Dependency Injection in Multi-User Web Application: Second User to Log In Causes First User To See Second User's Data

I'm trying to implement a web application using ASP.NET MVC and the Microsoft Unity DI framework. The application needs to support multiple user sessions at the same time, each of them with their own connection to a separate database (but all users using the same DbContext; the database schemas are identical, it's just the data that is different).
Upon a user's log-in, I register the necessary type mappings to the application's Unity container, using a session-based lifetime manager that I found in another question here.
My container is initialized like this:
// Global.asax.cs
public static UnityContainer CurrentUnityContainer { get; set; }
protected void Application_Start()
{
// ...other code...
CurrentUnityContainer = UnityConfig.Initialize();
// misc services - nothing data access related, apart from the fact that they all depend on IRepository<ClientContext>
UnityConfig.RegisterComponents(CurrentUnityContainer);
}
// UnityConfig.cs
public static UnityContainer Initialize()
{
UnityContainer container = new UnityContainer();
DependencyResolver.SetResolver(new UnityDependencyResolver(container));
GlobalConfiguration.Configuration.DependencyResolver = new Unity.WebApi.UnityDependencyResolver(container);
return container;
}
This is the code that's called upon logging in:
// UserController.cs
UnityConfig.RegisterUserDataAccess(MvcApplication.CurrentUnityContainer, UserData.Get(model.AzureUID).CurrentDatabase);
// UnityConfig.cs
public static void RegisterUserDataAccess(IUnityContainer container, string databaseName)
{
container.AddExtension(new DataAccessDependencies(databaseName));
}
// DataAccessDependencies.cs
public class DataAccessDependencies : UnityContainerExtension
{
private readonly string _databaseName;
public DataAccessDependencies(string databaseName)
{
_databaseName = databaseName;
}
protected override void Initialize()
{
IConfigurationBuilder configurationBuilder = Container.Resolve<IConfigurationBuilder>();
Container.RegisterType<ClientContext>(new SessionLifetimeManager(), new InjectionConstructor(configurationBuilder.GetConnectionString(_databaseName)));
Container.RegisterType<IRepository<ClientContext>, RepositoryService<ClientContext>>(new SessionLifetimeManager());
}
}
// SessionLifetimeManager.cs
public class SessionLifetimeManager : LifetimeManager
{
private readonly string _key = Guid.NewGuid().ToString();
public override void RemoveValue(ILifetimeContainer container = null)
{
HttpContext.Current.Session.Remove(_key);
}
public override void SetValue(object newValue, ILifetimeContainer container = null)
{
HttpContext.Current.Session[_key] = newValue;
}
public override object GetValue(ILifetimeContainer container = null)
{
return HttpContext.Current.Session[_key];
}
protected override LifetimeManager OnCreateLifetimeManager()
{
return new SessionLifetimeManager();
}
}
This works fine as long as only one user is logged in at a time. The data is fetched properly, the dashboards work as expected, and everything's just peachy keen.
Then, as soon as a second user logs in, disaster strikes.
The last user to have prompted a call to RegisterUserDataAccess seems to always have "priority"; their data is displayed on the dashboard, and nothing else. Whether this is initiated by a log-in, or through a database access selection in my web application that calls the same method to re-route the user's connection to another database they have permission to access, the last one to draw always imposes their data on all other users of the web application. If I understand correctly, this is a problem the SessionLifetimeManager was supposed to solve - unfortunately, I really can't seem to get it to work.
I sincerely doubt that a simple and common use-case like this - multiple users logged into an MVC application who each are supposed to access their own, separate data - is beyond the abilities of Unity, so obviously, I must be doing something very wrong here. Having spent most of my day searching through depths of the internet I wasn't even sure truly existed, I must, unfortunately, now realize that I am at a total and utter loss here.
Has anyone dealt with this issue before? Has anyone dealt with this use-case before, and if yes, can anyone tell me how to change my approach to make this a little less headache-inducing? I am utterly desperate at this point and am considering rewriting my entire data access methodology just to make it work - not the healthiest mindset for clean and maintainable code.
Many thanks.
the issue seems to originate from your registration call, when registering the same type multiple times with unity, the last registration call wins, in this case, that will be data access object for whoever user logs-in last. Unity will take that as the default registration, and will create instances that have the connection to that user's database.
The SessionLifetimeManager is there to make sure you get only one instance of the objects you resolve under one session.
One option to solve this is to use named registration syntax to register the data-access types under a key that maps to the logged-in user (could be the database name), and on the resolve side, retrieve this user key, and use it resolve the corresponding data access implementation for the user
Thank you, Mohammed. Your answer has put me on the right track - I ended up finally solving this using a RepositoryFactory which is instantiated in an InjectionFactory during registration and returns a repository that always wraps around a ClientContext pointing to the currently logged on user's currently selected database.
// DataAccessDependencies.cs
protected override void Initialize()
{
IConfigurationBuilder configurationBuilder = Container.Resolve<IConfigurationBuilder>();
Container.RegisterType<IRepository<ClientContext>>(new InjectionFactory(c => {
ClientRepositoryFactory repositoryFactory = new ClientRepositoryFactory(configurationBuilder);
return repositoryFactory.GetRepository();
}));
}
// ClientRepositoryFactory.cs
public class ClientRepositoryFactory : IRepositoryFactory<RepositoryService<ClientContext>>
{
private readonly IConfigurationBuilder _configurationBuilder;
public ClientRepositoryFactory(IConfigurationBuilder configurationBuilder)
{
_configurationBuilder = configurationBuilder;
}
public RepositoryService<ClientContext> GetRepository()
{
var connectionString = _configurationBuilder.GetConnectionString(UserData.Current.CurrentPermission);
ClientContext ctx = new ClientContext(connectionString);
RepositoryService<ClientContext> repository = new RepositoryService<ClientContext>(ctx);
return repository;
}
}
// UserData.cs (multiton-singleton-hybrid)
public static UserData Current
{
get
{
var currentAADUID = (string)(HttpContext.Current.Session["currentAADUID"]);
return Get(currentAADUID);
}
}
public static UserData Get(string AADUID)
{
UserData instance;
lock(_instances)
{
if(!_instances.TryGetValue(AADUID, out instance))
{
throw new UserDataNotInitializedException();
}
}
return instance;
}
public static UserData Current
{
get
{
var currentAADUID = (string)(HttpContext.Current.Session["currentAADUID"]);
return Get(currentAADUID);
}
}
public static UserData Get(string AADUID)
{
UserData instance;
lock(_instances)
{
if(!_instances.TryGetValue(AADUID, out instance))
{
throw new UserDataNotInitializedException();
}
}
return instance;
}

Adding extra step to ASP.NET MVC authentication

I have an MVC 5 website running using standard forms authentication.
However I need to add an extra step to the user's login process. Once the user has been authenticated we look up whether or not they have access to multiple offices. If they do we need to show them a list of offices and they must choose one.
This is a mandatory step and they cannot be considered logged on until they do it.
Do we need to create our own authentication or should I add a check to a BaseController?
You can extend the implementation of the built-in authentication:
public class OfficeSelectionAuthorizeAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var result = base.AuthorizeCore(httpContext);
if (result)
{
if (IsOfficeSelected())
{
return true;
}
httpContext.Response.RedirectToRoute("OfficeSelection Route");
httpContext.Response.Flush();
}
return false;
}
private bool IsOfficeSelected()
{
//office selection check
}
}
Then you need to use this filter instead of the default one:
[OfficeSelectionAuthorize]
public class AccountController : Controller
{
//action methods
}

Using Forms Authentication with Web API

I've got a Web Forms application which I'm trying to use the new Web API beta with. The endpoints I'm exposing should only be available to an authenticated user of the site since they're for AJAX use. In my web.config I have it set to deny all users unless they're authenticated. This works as it should with Web Forms but does not work as expected with MVC or the Web API.
I've created both an MVC Controller and Web API Controller to test with. What I'm seeing is that I can't access the MVC or Web API endpoints untill I authenticate but then I can continue hitting those endpoints, even after closing my browser and recyling the app pool. But if I hit one of my aspx pages, which sends me back to my login page, then I can't hit the MVC or Web API endpoints untill I authenticate again.
Is there a reason why MVC and Web API are not functioning as my ASPX pages are once my session is invalidated? By the looks of it only the ASPX request is clearing my Forms Authentication cookie, which I'm assuming is the issue here.
If your web API is just used within an existing MVC application, my advice is to create a custom AuthorizeAttribute filter for both your MVC and WebApi controllers; I create what I call an "AuthorizeSafe" filter, which blacklists everything by default so that if you forget to apply an authorization attribute to the controller or method, you are denied access (I think the default whitelist approach is insecure).
Two attribute classes are provided for you to extend; System.Web.Mvc.AuthorizeAttribute and System.Web.Http.AuthorizeAttribute; the former is used with MVC forms authentication and the latter also hooks into forms authentication (this is very nice because it means you don't have to go building a whole separate authentication architecture for your API authentication and authorization). Here's what I came up with - it denies access to all MVC controllers/actions and WebApi controllers/actions by default unless an AllowAnonymous or AuthorizeSafe attribute is applied. First, an extension method to help with custom attributes:
public static class CustomAttributeProviderExtensions {
public static List<T> GetCustomAttributes<T>(this ICustomAttributeProvider provider, bool inherit) where T : Attribute {
List<T> attrs = new List<T>();
foreach (object attr in provider.GetCustomAttributes(typeof(T), false)) {
if (attr is T) {
attrs.Add(attr as T);
}
}
return attrs;
}
}
The authorization helper class that both the AuthorizeAttribute extensions use:
public static class AuthorizeSafeHelper {
public static AuthActionToTake DoSafeAuthorization(bool anyAllowAnonymousOnAction, bool anyAllowAnonymousOnController, List<AuthorizeSafeAttribute> authorizeSafeOnAction, List<AuthorizeSafeAttribute> authorizeSafeOnController, out string rolesString) {
rolesString = null;
// If AllowAnonymousAttribute applied to action or controller, skip authorization
if (anyAllowAnonymousOnAction || anyAllowAnonymousOnController) {
return AuthActionToTake.SkipAuthorization;
}
bool foundRoles = false;
if (authorizeSafeOnAction.Count > 0) {
AuthorizeSafeAttribute foundAttr = (AuthorizeSafeAttribute)(authorizeSafeOnAction.First());
foundRoles = true;
rolesString = foundAttr.Roles;
}
else if (authorizeSafeOnController.Count > 0) {
AuthorizeSafeAttribute foundAttr = (AuthorizeSafeAttribute)(authorizeSafeOnController.First());
foundRoles = true;
rolesString = foundAttr.Roles;
}
if (foundRoles && !string.IsNullOrWhiteSpace(rolesString)) {
// Found valid roles string; use it as our own Roles property and auth normally
return AuthActionToTake.NormalAuthorization;
}
else {
// Didn't find valid roles string; DENY all access by default
return AuthActionToTake.Unauthorized;
}
}
}
public enum AuthActionToTake {
SkipAuthorization,
NormalAuthorization,
Unauthorized,
}
The two extension classes themselves:
public sealed class AuthorizeSafeFilter : System.Web.Mvc.AuthorizeAttribute {
public override void OnAuthorization(AuthorizationContext filterContext) {
if (!string.IsNullOrEmpty(this.Roles) || !string.IsNullOrEmpty(this.Users)) {
throw new Exception("This class is intended to be applied to an MVC web API application as a global filter in RegisterWebApiFilters, not applied to individual actions/controllers. Use the AuthorizeSafeAttribute with individual actions/controllers.");
}
string rolesString;
AuthActionToTake action = AuthorizeSafeHelper.DoSafeAuthorization(
filterContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>(false).Count() > 0,
filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>(false).Count() > 0,
filterContext.ActionDescriptor.GetCustomAttributes<AuthorizeSafeAttribute>(false),
filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AuthorizeSafeAttribute>(false),
out rolesString
);
string rolesBackup = this.Roles;
try {
switch (action) {
case AuthActionToTake.SkipAuthorization:
return;
case AuthActionToTake.NormalAuthorization:
this.Roles = rolesString;
base.OnAuthorization(filterContext);
return;
case AuthActionToTake.Unauthorized:
filterContext.Result = new HttpUnauthorizedResult();
return;
}
}
finally {
this.Roles = rolesBackup;
}
}
}
public sealed class AuthorizeSafeApiFilter : System.Web.Http.AuthorizeAttribute {
public override void OnAuthorization(HttpActionContext actionContext) {
if (!string.IsNullOrEmpty(this.Roles) || !string.IsNullOrEmpty(this.Users)) {
throw new Exception("This class is intended to be applied to an MVC web API application as a global filter in RegisterWebApiFilters, not applied to individual actions/controllers. Use the AuthorizeSafeAttribute with individual actions/controllers.");
}
string rolesString;
AuthActionToTake action = AuthorizeSafeHelper.DoSafeAuthorization(
actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0,
actionContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0,
actionContext.ActionDescriptor.GetCustomAttributes<AuthorizeSafeAttribute>().ToList(),
actionContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AuthorizeSafeAttribute>().ToList(),
out rolesString
);
string rolesBackup = this.Roles;
try {
switch (action) {
case AuthActionToTake.SkipAuthorization:
return;
case AuthActionToTake.NormalAuthorization:
this.Roles = rolesString;
base.OnAuthorization(actionContext);
return;
case AuthActionToTake.Unauthorized:
HttpRequestMessage request = actionContext.Request;
actionContext.Response = request.CreateResponse(HttpStatusCode.Unauthorized);
return;
}
}
finally {
this.Roles = rolesBackup;
}
}
}
And finally, the attribute that can be applied to methods/controllers to allow users in certain roles to access them:
public class AuthorizeSafeAttribute : Attribute {
public string Roles { get; set; }
}
Then we register our "AuthorizeSafe" filters globally from Global.asax:
public static void RegisterGlobalFilters(GlobalFilterCollection filters) {
// Make everything require authorization by default (whitelist approach)
filters.Add(new AuthorizeSafeFilter());
}
public static void RegisterWebApiFilters(HttpFilterCollection filters) {
// Make everything require authorization by default (whitelist approach)
filters.Add(new AuthorizeSafeApiFilter());
}
Then to open up an action to eg. anonymous access or only Admin access:
public class AccountController : System.Web.Mvc.Controller {
// GET: /Account/Login
[AllowAnonymous]
public ActionResult Login(string returnUrl) {
// ...
}
}
public class TestApiController : System.Web.Http.ApiController {
// GET API/TestApi
[AuthorizeSafe(Roles="Admin")]
public IEnumerable<TestModel> Get() {
return new TestModel[] {
new TestModel { TestId = 123, TestValue = "Model for ID 123" },
new TestModel { TestId = 234, TestValue = "Model for ID 234" },
new TestModel { TestId = 345, TestValue = "Model for ID 345" }
};
}
}
It should work in Normal MVC controller. you just need to decorate the action with [Authorize] attribute.
In web api you need to have custom authorization. you may find below link helpful.
http://www.codeproject.com/Tips/376810/ASP-NET-WEB-API-Custom-Authorize-and-Exception-Han
If you are using the MVC Authorize attribute it should work the same way on for the WebAPI as for normal MVC controllers.

Asp.Net MVC Authentication Roles without Providers

I know this has been answered here before, however even after following all the solutions I could find, I cannot still get my roles working in my system.
I have a Asp.Net MVC application, with Forms based authentication. Instead of using a local database, it uses OpenAuth/OpenID for authentication, and a database lookup table for application roles.
As per main suggestion, I implemented the roles in Global.asax like:
protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
//Fires upon attempting to authenticate the use
if (HttpContext.Current.User != null &&
HttpContext.Current.User.Identity.IsAuthenticated &&
HttpContext.Current.User.Identity.GetType() == typeof (FormsIdentity))
Thread.CurrentPrincipal = HttpContext.Current.User = OpenAuthPrincipal.Get(HttpContext.Current.User.Identity.Name);
}
Here OpenAuthPrincipal.Get is a very straightforward static method wrapping the openauth id with the roles:
public static IPrincipal Get(string userId)
{
var db = new WebData();
var user = db.Users.Find(userId);
return new GenericPrincipal(new Identity(user), user.Roles.Split('|'));
}
However when I reach a function like:
[Authorize(Roles = "Admin")]
public ActionResult Edit(int id)
{
...
}
It fails. If I remove the Roles restriction, and check User.IsInRole("Admin") in the debugger I get a false. However, if I do the check in the Global.asax, I get true.
I know that the User.Identity.Name is coming correctly. And also the IIdentity is not modified at all. However only the roles are lost.
What could be the cause of this issue?
Update:
The solution recommended below did not directly work, however this change fixed the issue for me:
protected override bool AuthorizeCore(System.Web.HttpContextBase httpContext)
{
httpContext.User = OpenAuthPrincipal.Get(httpContext.User.Identity.Name);
return base.AuthorizeCore(httpContext);
}
As per main suggestion, I implemented the roles in Global.asax like:
No idea where did you get this main suggestion from but in ASP.NET MVC you normally use authorization action filters. And since the default Authorize filter doesn't do what you need, you write your own:
public class OpenIdAuthorizeAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var authorized = base.AuthorizeCore(httpContext);
if (authorized)
{
httpContext.User = OpenAuthPrincipal.Get(httpContext.User.Identity.Name);
}
return authorized;
}
}
and then:
[OpenIdAuthorize(Roles = "Admin")]
public ActionResult Edit(int id)
{
...
}

Does the WebAii framework support a page class structure in the way that WatiN does?

I am evaluating a couple of different frameworks for test automation. One of the things I really like about WatiN is the page model for abstracting page code from your tests.
Watin Example for a login Page:
public class AVLoginPage : Page
{
public TextField Email
{
get { return Document.TextField(Find.ById("UserLogin_UserName")); }
}
public TextField Password
{
get { return Document.TextField(Find.ById("UserLogin_Password")); }
}
public Button LoginBtn
{
get { return Document.Button(Find.ById("UserLogin_LoginButton")); }
}
/// <summary>
/// Enters the email and loging in to the corresponding boxes and clicks the login button.
/// </summary>
/// <param name="email"></param>
/// <param name="password"></param>
public void Login(string email, string password)
{
Email.TypeText(email);
Password.TypeText(password);
LoginBtn.Click();
}
}
Can I do something like this with WebAii?
So here is the approach I have started to take using the WebAii libraries:
My test code looks like:
[TestMethod]
public void Login_inValid_Combination_WebAii()
{
Manager.LaunchNewBrowser(BrowserType.Safari);
ActiveBrowser.NavigateTo(baseUrl + "login.aspx");
LoginPage.Login("test#roger.com", "123421343414",ActiveBrowser);
string expectedMsg = "Email address or password is incorrect.";
string actualMsg = LoginPage.GetError(ActiveBrowser);
Assert.IsTrue(actualMsg.Contains(expectedMsg));
}
I then have a library:
using ArtOfTest.WebAii.Controls.HtmlControls;
using ArtOfTest.WebAii.Controls.HtmlControls.HtmlAsserts;
using ArtOfTest.WebAii.Core;
using ArtOfTest.WebAii.ObjectModel;
using ArtOfTest.WebAii.TestAttributes;
using ArtOfTest.WebAii.TestTemplates;
using ArtOfTest.WebAii.Win32.Dialogs;
using ArtOfTest.WebAii.Silverlight;
using ArtOfTest.WebAii.Silverlight.UI;
namespace WebAIIPageLibrary
{
public class LoginPage : BaseTest
{
public static void Login(string email, string password, Browser passedBrowser )
{
passedBrowser.Find.ById<HtmlInputText>("UserLogin_UserName").Text = email;
passedBrowser.Find.ById<HtmlInputPassword>("UserLogin_Password").Text = password;
passedBrowser.Find.ById<HtmlInputSubmit>("UserLogin_LoginButton").Click();
}
public static string GetError(Browser passedBrowser)
{
ReadOnlyCollection<HtmlDiv> div = passedBrowser.Find.AllByTagName<HtmlDiv>("div");
string errorMsg = "";
foreach(HtmlDiv s in div)
{
if (s.CssClass == "error")
{
errorMsg = s.InnerText;
break;
}
}
return errorMsg;
}
public static string GetDashboardTitle(Browser passedBrowser)
{
return passedBrowser.Window.Caption;
}
}
}
This allows me to abstract the actions on the page from the test code itself.
Telerik Testing Framework (formerly called WebAii) does not include any recording capabilities. To get full recording and automatic Page class abstraction the way I think you want it you need to purchase a license to Test Studio (or Test Studio Express which comes with Ultimate Collection).
If you want to hand code your own abstractions, you are welcome to do so following the Find Expression documentation. Also (not currently documented, but we're working on it) is the [Find()] attribute you can use along with Find Expressions. This attribute replaces the old FindParam attribute. The FindParam attribute only work on HTML elements while the new Find attribute works on both HTML and XAML elements.

Resources