Extending Umbraco ProfileModel with custom member model - asp.net-membership

I have the following code while updating the member account detail information:
/// <summary>
/// Verifies and edits the member fields.
/// </summary>
/// <param name="model"></param>
/// <returns>MemberDetailsFormViewModel containing all the editable information.</returns>
[Authorize]
public ActionResult HandleUpdateMemberDetails(MemberDetailsFormViewModel model)
{
if (ModelState.IsValid == false)
{
return CurrentUmbracoPage();
}
var memberService = Services.MemberService;
var member = memberService.GetById(Members.GetCurrentMemberId());
member.Properties[Constant.PropertyAlias.Authentication.FirstName].Value = model.FirstName;
member.Properties[Constant.PropertyAlias.Authentication.LastName].Value = model.LastName;
member.Properties[Constant.PropertyAlias.Authentication.AddressLine1].Value = model.AddressLine1;
member.Properties[Constant.PropertyAlias.Authentication.AddressLine2].Value = model.AddressLine2;
member.Properties[Constant.PropertyAlias.Authentication.TownCity].Value = model.TownCity;
member.Properties[Constant.PropertyAlias.Authentication.PostCode].Value = model.PostCode;
member.Properties[Constant.PropertyAlias.Authentication.County].Value = model.County;
member.Properties[Constant.PropertyAlias.Authentication.Country].Value = model.Country;
member.Properties[Constant.PropertyAlias.Authentication.PhoneNumber].Value = model.PhoneNumber;
memberService.Save(member);
if (Members.IsLoggedIn())
{
ViewBag.DetailSuccessfullyChanged = 1;
return CurrentUmbracoPage();
}
return View("/");
}
Although this solution works, I would like to do more elegant updating of the member.Properties["propertyName"].Value through extended ProfileModel with the following method of MembershipHelper: UpdateMemberProfile(ProfileModel model).
Useful links:
MembershipHelper documentation link.
Similar issue on OUR Umbraco
Has anyone managed to do something like this?
EDIT:
I see that Warren did something similiar on his GitHub.

Related

ABP.IO - MultiTenancy - Setting Tenant from External IDP

I am trying to configure Auth0 as an external login provider in my ABP.IO application (MVC with integrated identity server). I've got it working so that I can log in fine, but what I can't figure out is how to set the tenant in the ABP side.
What I came up with is a rule on the Auth0 side to populate the TenantId as a claim in the id token, so I can parse that in my custom SingInManager in the GetExternalLoginInfoAsync method, like so:
string tenantId = auth.Principal.FindFirstValue("https://example.com/tenantId");
I'm just having a hard time figuring out what to do with it from there. The assumption is that users will be configured to authenticate via Auth0 and the users will get created locally on first login (which, again, is working EXCEPT for the Tenant part).
Alright, here is the workaround I have in place, and it SHOULD be transferable to any external login system that you are depending on. I'm not sure if this is the correct way of doing this, so if anybody else wants to chip in with a more efficient system I am all ears.
Anyway, my workflow assumes that you have, like I did, created a mechanism for the TenantId to be sent from the external IDP. For this, I used the Organizations feature in Auth0 and added the TenantId as metadata, then I created an Action in Auth0 to attach that metadata as a claim to be used on the ABP side.
In ABP, I followed this article to override the SignInManager: https://community.abp.io/articles/how-to-customize-the-signin-manager-3e858753
As in the article, I overrode the GetExternalLoginInfoAsync method of the sign in manager and added the following lines to pull the TenantId out of the Auth0 claims and add it back in using the pre-defined AbpClaimTypes.TenantId value.
EDIT: I also had to override the ExternalLoginSignInAsync method to account for multi-tenancy (otherwise it kept trying to recreate the users and throwing duplicate email errors). I'll post the full class below with my added stuff in comments:
public class CustomSignInManager : Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser>
{
private const string LoginProviderKey = "LoginProvider";
private const string XsrfKey = "XsrfId";
private readonly IDataFilter _dataFilter;
public CustomSignInManager(
IDataFilter dataFilter,
Microsoft.AspNetCore.Identity.UserManager<Volo.Abp.Identity.IdentityUser> userManager,
Microsoft.AspNetCore.Http.IHttpContextAccessor contextAccessor,
Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory<Volo.Abp.Identity.IdentityUser> claimsFactory,
Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Identity.IdentityOptions> optionsAccessor,
Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser>> logger,
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemes,
Microsoft.AspNetCore.Identity.IUserConfirmation<Volo.Abp.Identity.IdentityUser> confirmation)
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
{
_dataFilter = dataFilter;
}
/// <summary>
/// Gets the external login information for the current login, as an asynchronous operation.
/// </summary>
/// <param name="expectedXsrf">Flag indication whether a Cross Site Request Forgery token was expected in the current request.</param>
/// <returns>The task object representing the asynchronous operation containing the <see name="ExternalLoginInfo"/>
/// for the sign-in attempt.</returns>
public override async Task<Microsoft.AspNetCore.Identity.ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null)
{
var auth = await Context.AuthenticateAsync(IdentityConstants.ExternalScheme);
var items = auth?.Properties?.Items;
if (auth?.Principal == null || items == null || !items.ContainsKey(LoginProviderKey))
{
return null;
}
if (expectedXsrf != null)
{
if (!items.ContainsKey(XsrfKey))
{
return null;
}
var userId = items[XsrfKey] as string;
if (userId != expectedXsrf)
{
return null;
}
}
var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
var provider = items[LoginProviderKey] as string;
if (providerKey == null || provider == null)
{
return null;
}
var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName
?? provider;
/* Begin tenantId claim search */
string tenantId = auth.Principal.FindFirstValue("https://example.com/tenantId"); //pull the tenantId claim if it exists
if(!string.IsNullOrEmpty(tenantId))
{
auth.Principal.Identities.FirstOrDefault().AddClaim(new Claim(AbpClaimTypes.TenantId, tenantId)); //if there is a tenantId, add the AbpClaimTypes.TenantId claim back into the principal
}
/* End tenantId claim search */
var eli = new ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName)
{
AuthenticationTokens = auth.Properties.GetTokens(),
AuthenticationProperties = auth.Properties
};
return eli;
}
/// <summary>
/// Signs in a user via a previously registered third party login, as an asynchronous operation.
/// </summary>
/// <param name="loginProvider">The login provider to use.</param>
/// <param name="providerKey">The unique provider identifier for the user.</param>
/// <param name="isPersistent">Flag indicating whether the sign-in cookie should persist after the browser is closed.</param>
/// <param name="bypassTwoFactor">Flag indicating whether to bypass two factor authentication.</param>
/// <returns>The task object representing the asynchronous operation containing the <see name="SignInResult"/>
/// for the sign-in attempt.</returns>
public override async Task<SignInResult> ExternalLoginSignInAsync(string loginProvider, string providerKey, bool isPersistent, bool bypassTwoFactor)
{
Volo.Abp.Identity.IdentityUser user = null; //stage the user variable as null
using (_dataFilter.Disable<IMultiTenant>()) //disable the tenantid filters so we can search all logins for the expected key
{
user = await UserManager.FindByLoginAsync(loginProvider, providerKey); //search logins for the expected key
}
if (user == null)
{
return SignInResult.Failed;
}
var error = await PreSignInCheck(user);
if (error != null)
{
return error;
}
return await SignInOrTwoFactorAsync(user, isPersistent, loginProvider, bypassTwoFactor);
}
}
Once that was done, I tracked down where the GetExternalLoginInfoAsync was being utilized and figured out I had to override the CreateExternalUserAsync method inside of the LoginModel for the Login page. To that end, I followed the directions in this article for creating a CustomLoginModel.cs and Login.cshtml : https://community.abp.io/articles/hide-the-tenant-switch-of-the-login-page-4foaup7p
So, my Auth0LoginModel class looks like this:
public class Auth0LoginModel : LoginModel
{
public Auth0LoginModel(IAuthenticationSchemeProvider schemeProvider, IOptions<AbpAccountOptions> accountOptions, IOptions<IdentityOptions> identityOptions) : base(schemeProvider, accountOptions, identityOptions)
{
}
protected override async Task<IdentityUser> CreateExternalUserAsync(ExternalLoginInfo info)
{
await IdentityOptions.SetAsync();
var emailAddress = info.Principal.FindFirstValue(AbpClaimTypes.Email);
/* Begin TenantId claim check */
var tenantId = info.Principal.FindFirstValue(AbpClaimTypes.TenantId);
if (!string.IsNullOrEmpty(tenantId))
{
try
{
CurrentTenant.Change(Guid.Parse(tenantId));
}
catch
{
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext()
{
Identity = IdentitySecurityLogIdentityConsts.IdentityExternal,
Action = "Unable to parse TenantId: " + tenantId
}) ;
}
}
/* End TenantId claim check */
var user = new IdentityUser(GuidGenerator.Create(), emailAddress, emailAddress, CurrentTenant.Id);
CheckIdentityErrors(await UserManager.CreateAsync(user));
CheckIdentityErrors(await UserManager.SetEmailAsync(user, emailAddress));
CheckIdentityErrors(await UserManager.AddLoginAsync(user, info));
CheckIdentityErrors(await UserManager.AddDefaultRolesAsync(user));
return user;
}
}
The code added is between the comments, the rest of the method was pulled from the source. So I look for the AbpClaimTypes.TenantId claim being present, and if it does I attempt to use the CurrentTenant.Change method to change the tenant prior to the call to create the new IdentityUser.
Once that is done, the user gets created in the correct tenant and everything flows like expected.

Working on a Dynamics CRM Custom Workflow that updates "Modified By" field -- need help debugging

I am working on a Dynamics CRM CWA that updates the "Modified By" field based on a text field called "Prepared By". I currently have 3 errors that I need some help debugging (see below). They may be pretty easy fixes but I am fairly new to coding. Any help de-bugging would be greatly appreciated. Thanks!
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Threading.Tasks;
using System.Activities;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Workflow;
using System.Runtime.Serialization;
namespace KED365.Workflows
{
/// </summary>
public class ModifiedBy : WorkFlowActivityBase
{
private Guid contactid;
[Input("User Full Name")]
public InArgument<string> UserFullName { get; set; }
/// <summary>
/// Executes the WorkFlow.
/// </summary>
/// <param name="crmWorkflowContext">The <see cref="LocalWorkflowContext"/> which contains the
/// <param name="executionContext" > <see cref="CodeActivityContext"/>
/// </param>
/// <remarks>
/// For improved performance, Microsoft Dynamics 365 caches WorkFlow instances.
/// The WorkFlow's Execute method should be written to be stateless as the constructor
/// is not called for every invocation of the WorkFlow. Also, multiple system threads
/// could execute the WorkFlow at the same time. All per invocation state information
/// is stored in the context. This means that you should not use global variables in WorkFlows.
/// </remarks>
protected override void Execute(CodeActivityContext activityContext, IWorkflowContext workflowContext, IOrganizationService orgService, ITracingService tracingService)
{
//get entity record for which plugin was fired
Entity _target = (Entity)workflowContext.InputParameters["Target"];
//check if portaluser name is to be obtained from custom createby or from custom modifiedby
if (workflowContext.MessageName.ToUpper() == "CREATE")
{
contactid = _target.Attributes.Contains("new_createdby") ? _target.GetAttributeValue<EntityReference>("new_createdby").Id : Guid.Empty;
}
else
{
contactid = _target.Attributes.Contains("new_modifiedby") ? _target.GetAttributeValue<EntityReference>("new_modifiedby").Id : Guid.Empty;
}
//retrieve contact fullname from contactid
var _contact = activityContext.CreateQuery("contact").Where(c => c.GetAttributeValue<Guid>("contactid").Equals(contactid)).FirstOrDefault();
if (_contact != null)
{
if (_contact.Attributes.Contains("fullname"))
{
fullname = _contact.GetAttributeValue<string>("fullname");
}
//retrieve Systemuser that has same name as that of new_portalcreatedby/ //new_portalmodifiedby
Entity _user = context.CreateQuery("systemuser").Where(e => e.GetAttributeValue<string>("fullname").Equals(fullname)).FirstOrDefault();
if (_user != null)
{
//check if we need to update createdby or modifiedby
if (workflowContext.MessageName.ToUpper() == "CREATE")
{
_target["createdby"] = _user.ToEntityReference();
}
else
{
_target["modifiedby"] = _user.ToEntityReference();
}
//assign new target to plugin executioncontext
workflowContext.InputParameters["Target"] = _target;
}
}
}
}
}
Error 1 :
Severity Code Description Project File Line Suppression State
Error CS1061 'CodeActivityContext' does not contain a definition for 'CreateQuery' and no extension method 'CreateQuery' accepting a first argument of type 'CodeActivityContext' could be found (are you missing a using directive or an assembly reference?) Workflows C:\Users\tgiard\Downloads\GetUserByName-master\GetUserByName-master\Workflows\ModifiedBy.cs 68 Active
Error 2 :
Severity Code Description Project File Line Suppression State
Error CS0103 The name 'fullname' does not exist in the current context Workflows C:\Users\tgiard\Downloads\GetUserByName-master\GetUserByName-master\Workflows\ModifiedBy.cs 75 Active
Error 3 :
Severity Code Description Project File Line Suppression State
Error CS0103 The name 'context' does not exist in the current context Workflows C:\Users\tgiard\Downloads\GetUserByName-master\GetUserByName-master\Workflows\ModifiedBy.cs 79 Active
Here is some feedback on your issues:
Error 1 - 'CodeActivityContext' does not contain a definition for 'CreateQuery'
This issue is related to the lines:
var _contact = activityContext.CreateQuery("contact").Where(c => c.GetAttributeValue<Guid>("contactid").Equals(contactid)).FirstOrDefault();
and
Entity _user = context.CreateQuery("systemuser").Where(e => e.GetAttributeValue<string>("fullname").Equals(fullname)).FirstOrDefault();
I don't know what that method is but you have better options; for the contact you already have the guid, so you can simply use a Retrieve():
var _contact = orgService.Retrieve("contact", contactid, new ColumnSet("fullname"));
And for the system user write a QueryExpression filtering by fullname:
var query = new QueryExpression("systemuser"):
query.Criteria.AddCondition("fullname", ConditionOperator.Equal, fullname);
var _user = orgService.RetrieveMultiple(query).Entities.FirstOrDefault();
Error 2: The name 'fullname' does not exist in the current contex
This is basic C#, you must instantiate your variable before you use it:
string fullname;
Error 3: The name 'context' does not exist in the current context
Ironic and true. This should be activityContext, but we have already fixed this issue in the change we made for Error 1.
Entity _user = context.CreateQuery("systemuser").Where(e => e.GetAttributeValue<string>("fullname").Equals(fullname)).FirstOrDefault();
As Zach Mast correctly pointed out, using a Pre-Operation is recommended. It also seems an odd case where you retrieve a contacts name and match it with a user. Instead, You could change the type of the field to a user reference, add a user field to the contact you retrieve or add a code to match the Contact to the User. This way, you won't have issues with users with the same name or typo's.
Please find below your workflow activity converted to a Pre-Operation plugin.
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Linq;
namespace KED365.Plugins
{
public class CreateUpdateContact : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
var tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
var factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
var service = factory.CreateOrganizationService(context.UserId);
tracingService.Trace("Start plugin");
tracingService.Trace("Validate Target");
if (!context.InputParameters.Contains("Target") || !(context.InputParameters["Target"] is Entity))
return;
tracingService.Trace("Retrieve Target");
var target = (Entity)context.InputParameters["Target"];
String message = context.MessageName.ToLower();
SetCreatedByAndModifiedBy(tracingService, service, target, message);
}
private void SetCreatedByAndModifiedBy(ITracingService tracingService, IOrganizationService service, Entity target, string message)
{
tracingService.Trace("Start SetPriceList");
tracingService.Trace("Validate Message is Create or Update");
if (!message.Equals("create", StringComparison.OrdinalIgnoreCase) && !message.Equals("update", StringComparison.OrdinalIgnoreCase))
return;
tracingService.Trace("Retrieve Attributes");
var createdByReference = target.GetAttributeValue<EntityReference>("new_createdby");
var modifiedByReference = target.GetAttributeValue<EntityReference>("new_modifiedby");
tracingService.Trace("Retrieve And Set User for Created By");
RetrieveAndSetUser(tracingService, service, target, createdByReference, "createdby");
tracingService.Trace("Retrieve And Set User for Modified By");
RetrieveAndSetUser(tracingService, service, target, modifiedByReference, "modifiedby");
}
private void RetrieveAndSetUser(ITracingService tracingService, IOrganizationService service, Entity target, EntityReference reference, string targetAttribute)
{
tracingService.Trace("Validating Reference");
if (reference == null)
return;
tracingService.Trace("Retrieving and Validating User");
var user = RetrieveUserByName(service, reference.Name, new ColumnSet(false));
if (user == null)
return;
tracingService.Trace("Setting Target Attribute");
target[targetAttribute] = user.ToEntityReference();
}
private Entity RetrieveUserByName(IOrganizationService service, string name, ColumnSet columns)
{
var query = new QueryExpression
{
EntityName = "systemuser",
ColumnSet = columns,
Criteria = new FilterExpression
{
FilterOperator = LogicalOperator.And,
Conditions =
{
new ConditionExpression
{
AttributeName = "fullname",
Operator = ConditionOperator.Equal,
Values = { name }
}
}
}
};
var retrieveResponse = service.RetrieveMultiple(query);
if (retrieveResponse.Entities.Count == 1)
{
return retrieveResponse.Entities.FirstOrDefault();
}
else
{
// Alternatively you can thrown an error as you have unexpectedly multiple matches
return null;
}
}
}
}

How can i force the DbUpdateConcurrencyException to be raised even if i am passing the FormCollection to my Post action method instead of an object

i have the following action method inside my asp.net mvc web application , which will raise a DbUpdateConcurrencyException as intended to handle any concurrent conflicts that might happen:-
[HttpPost]
public ActionResult Edit(Assessment a)
{ try
{
if (ModelState.IsValid)
{
elearningrepository.UpdateAssessment(a);
elearningrepository.Save();
return RedirectToAction("Details", new { id = a.AssessmentID });
}
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var clientValues = (Assessment)entry.Entity;
ModelState.AddModelError(string.Empty, "The record you attempted to edit was"
+ "modified by another user after you got the original value.");
}
catch (DataException)
{
ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator.");
}
return View(a);}
but to avoid any over binding attacks i have define a [Bind(Include = "Date, Title")] on the object class, but this raised a problem to me as the above action method will return an exception even if no concurrent conflict occur becuase the model binder will not be able to bind the object ID and other values ,, so i have changed my action method to the following:-
[HttpPost]
public ActionResult Edit(int id, FormCollection collection)
{
Assessment a = elearningrepository.GetAssessment(id);
try
{
if (TryUpdateModel(a))
{
elearningrepository.UpdateAssessment(a);
elearningrepository.Save();
return RedirectToAction("Details", new { id = a.AssessmentID });
}
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var clientValues = (Assessment)entry.Entity;
ModelState.AddModelError(string.Empty, "The record you attempted to edit was"
+ "modified by another user after you got the original value.");
}
catch (DataException)
{ ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator.");
}return View(a);
but writting the action method as in the second approach will not raise the DbUpdateConcurrencyException under any situation (even if a concurrency conflict occurs!!!).
so me question is how i can make sure that the DbUpdateConcurrencyException will be raised if any conflict occur and at the same time to make sure that no over binding attack might occur by defining [Bind(Include = "Date, Title")]?
thanks in advance for any help and suggestions .
BR
Stop using forms collection and use a view model, thats a far better approach.
Also I have an action filter I wrote to handle the concurrency exceptions (MVC4 handles entity exceptions now finally has pat of validation just not the concurrency exceptions). Its a work in progress but should work ok as is, that much has been tested : )
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Mvc;
using System.Data.Entity.Infrastructure;
using System.Reflection;
namespace Gecko.Framework.Mvc.ActionFilters
{
/// <summary>
/// Author: Adam Tuliper
/// adam.tuliper#gmail.com
/// completedevelopment.blogspot.com
/// www.secure-coding.com
/// Use freely, just please retain original credit.
///
/// This attribute attempts to intercept DbUpdateConcurrencyException to write out original/new values
/// to the screen for the user to review.
/// It assumes the following:
/// 1. There is a [Timestamp] attribute on an entity framework model property
/// 2. The only differences that we care about from the posted data to the record currently in the database are
/// only yhe model state field. We do not have access to a model at this point, as an exception was raised so there was no
/// return View(model) that we have a model to process from.
/// As such, we have to look at the fields in the modelstate and try to find matching fields on the entity and then display the differences.
/// This may not work in all cases.
/// This class will look at your model to get the property names. It will then check your
/// Entities current values vs. db values for these property names.
/// The behavior can be changed.
/// </summary>
public class HandleConcurrencyException : FilterAttribute, IExceptionFilter //ActionFilterAttribute
{
private PropertyMatchingMode _propertyMatchingMode;
/// <summary>
/// This defines when the concurrencyexception happens,
/// </summary>
public enum PropertyMatchingMode
{
/// <summary>
/// Uses only the field names in the model to check against the entity. This option is best when you are using
/// View Models with limited fields as opposed to an entity that has many fields. The ViewModel (or model) field names will
/// be used to check current posted values vs. db values on the entity itself.
/// </summary>
UseViewModelNamesToCheckEntity = 0,
/// <summary>
/// Use any non-matching value fields on the entity (except timestamp fields) to add errors to the ModelState.
/// </summary>
UseEntityFieldsOnly = 1,
/// <summary>
/// Tells the filter to not attempt to add field differences to the model state.
/// This means the end user will not see the specifics of which fields caused issues
/// </summary>
DontDisplayFieldClashes = 2
}
/// <summary>
/// The main method, called by the mvc runtime when an exception has occured.
/// This must be added as a global filter, or as an attribute on a class or action method.
/// </summary>
/// <param name="filterContext"></param>
public void OnException(ExceptionContext filterContext)
{
if (!filterContext.ExceptionHandled && filterContext.Exception is DbUpdateConcurrencyException)
{
//Get original and current entity values
DbUpdateConcurrencyException ex = (DbUpdateConcurrencyException)filterContext.Exception;
var entry = ex.Entries.Single();
//problems with ef4.1/4.2 here because of context/model in different projects.
//var databaseValues = entry.CurrentValues.Clone().ToObject();
//var clientValues = entry.Entity;
//So - if using EF 4.1/4.2 you may use this workaround
var clientValues = entry.CurrentValues.Clone().ToObject();
entry.Reload();
var databaseValues = entry.CurrentValues.ToObject();
List<string> propertyNames;
filterContext.Controller.ViewData.ModelState.AddModelError(string.Empty, "The record you attempted to edit "
+ "was modified by another user after you got the original value. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again to cause your changes to be the current saved values.");
PropertyInfo[] entityFromDbProperties = databaseValues.GetType().GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance);
if (_propertyMatchingMode == PropertyMatchingMode.UseViewModelNamesToCheckEntity)
{
//We dont have access to the model here on an exception. Get the field names from modelstate:
propertyNames = filterContext.Controller.ViewData.ModelState.Keys.ToList();
}
else if (_propertyMatchingMode == PropertyMatchingMode.UseEntityFieldsOnly)
{
propertyNames = databaseValues.GetType().GetProperties(BindingFlags.Public).Select(o => o.Name).ToList();
}
else
{
filterContext.ExceptionHandled = true;
UpdateTimestampField(filterContext, entityFromDbProperties, databaseValues);
filterContext.Result = new ViewResult() { ViewData = filterContext.Controller.ViewData };
return;
}
UpdateTimestampField(filterContext, entityFromDbProperties, databaseValues);
//Get all public properties of the entity that have names matching those in our modelstate.
foreach (var propertyInfo in entityFromDbProperties)
{
//If this value is not in the ModelState values, don't compare it as we don't want
//to attempt to emit model errors for fields that don't exist.
//Compare db value to the current value from the entity we posted.
if (propertyNames.Contains(propertyInfo.Name))
{
if (propertyInfo.GetValue(databaseValues, null) != propertyInfo.GetValue(clientValues, null))
{
var currentValue = propertyInfo.GetValue(databaseValues, null);
if (currentValue == null || string.IsNullOrEmpty(currentValue.ToString()))
{
currentValue = "Empty";
}
filterContext.Controller.ViewData.ModelState.AddModelError(propertyInfo.Name, "Current value: "
+ currentValue);
}
}
//TODO: hmm.... how can we only check values applicable to the model/modelstate rather than the entity we saved?
//The problem here is we may only have a few fields used in the viewmodel, but many in the entity
//so we could have a problem here with that.
}
filterContext.ExceptionHandled = true;
filterContext.Result = new ViewResult() { ViewData = filterContext.Controller.ViewData };
}
}
public HandleConcurrencyException()
{
_propertyMatchingMode = PropertyMatchingMode.UseViewModelNamesToCheckEntity;
}
public HandleConcurrencyException(PropertyMatchingMode propertyMatchingMode)
{
_propertyMatchingMode = propertyMatchingMode;
}
/// <summary>
/// Searches the database loaded entity values for a field that has a [Timestamp] attribute.
/// It then writes a string version of ther byte[] timestamp out to modelstate, assuming
/// we have a timestamp field on the page that caused the concurrency exception.
/// </summary>
/// <param name="filterContext"></param>
/// <param name="entityFromDbProperties"></param>
/// <param name="databaseValues"></param>
private void UpdateTimestampField(ExceptionContext filterContext, PropertyInfo[] entityFromDbProperties, object databaseValues)
{
foreach (var propertyInfo in entityFromDbProperties)
{
var attributes = propertyInfo.GetCustomAttributesData();
//If this is a timestamp field, we need to set the current value.
foreach (CustomAttributeData attr in attributes)
{
if (typeof(System.ComponentModel.DataAnnotations.TimestampAttribute).IsAssignableFrom(attr.Constructor.DeclaringType))
{
//This currently works only with byte[] timestamps. You can use dates as timestampts, but support is not provided here.
byte[] timestampValue = (byte[])propertyInfo.GetValue(databaseValues, null);
//we've found the timestamp. Add it to the model.
filterContext.Controller.ViewData.ModelState.Add(propertyInfo.Name, new ModelState());
filterContext.Controller.ViewData.ModelState.SetModelValue(propertyInfo.Name,
new ValueProviderResult(Convert.ToBase64String(timestampValue), Convert.ToBase64String(timestampValue), null));
break;
}
}
}
}
}
}
Do you know why I had to change
if (propertyInfo.GetValue(databaseValues, null) != propertyInfo.GetValue(clientValues, null))
to
if (!Equals(propertyInfo.GetValue(databaseValues,null), propertyInfo.GetValue(clientValues,null)))
It was giving me false positives for anything that was not a string.
After I made that change it worked for me.
Here is the complete modified function
public void OnException(ExceptionContext filterContext)
{
if (!filterContext.ExceptionHandled && filterContext.Exception is DbUpdateConcurrencyException)
{
//Get original and current entity values
DbUpdateConcurrencyException ex = (DbUpdateConcurrencyException)filterContext.Exception;
var entry = ex.Entries.Single();
//problems with ef4.1/4.2 here because of context/model in different projects.
//var databaseValues = entry.CurrentValues.Clone().ToObject();
//var clientValues = entry.Entity;
//So - if using EF 4.1/4.2 you may use this workaround
var clientValues = entry.CurrentValues.Clone().ToObject();
entry.Reload();
var databaseValues = entry.CurrentValues.ToObject();
List<string> propertyNames;
filterContext.Controller.ViewData.ModelState.AddModelError(string.Empty, "The record you attempted to edit "
+ "was modified by another user after you got the original value. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again to cause your changes to be the current saved values.");
PropertyInfo[] entityFromDbProperties = databaseValues.GetType().GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance);
if (_propertyMatchingMode == PropertyMatchingMode.UseViewModelNamesToCheckEntity)
{
//We dont have access to the model here on an exception. Get the field names from modelstate:
propertyNames = filterContext.Controller.ViewData.ModelState.Keys.ToList();
}
else if (_propertyMatchingMode == PropertyMatchingMode.UseEntityFieldsOnly)
{
propertyNames = databaseValues.GetType().GetProperties(BindingFlags.Public).Select(o => o.Name).ToList();
}
else
{
filterContext.ExceptionHandled = true;
UpdateTimestampField(filterContext, entityFromDbProperties, databaseValues);
filterContext.Result = new ViewResult() { ViewData = filterContext.Controller.ViewData };
return;
}
UpdateTimestampField(filterContext, entityFromDbProperties, databaseValues);
//Get all public properties of the entity that have names matching those in our modelstate.
foreach (var propertyInfo in entityFromDbProperties)
{
//If this value is not in the ModelState values, don't compare it as we don't want
//to attempt to emit model errors for fields that don't exist.
//Compare db value to the current value from the entity we posted.
if (propertyNames.Contains(propertyInfo.Name))
{
//if (propertyInfo.GetValue(databaseValues, null) != propertyInfo.GetValue(clientValues, null))
if (!Equals(propertyInfo.GetValue(databaseValues,null), propertyInfo.GetValue(clientValues,null)))
{
var currentValue = propertyInfo.GetValue(databaseValues, null);
if (currentValue == null || string.IsNullOrEmpty(currentValue.ToString()))
{
currentValue = "Empty";
}
filterContext.Controller.ViewData.ModelState.AddModelError(propertyInfo.Name, "Database value: "
+ currentValue);
}
}
//TODO: hmm.... how can we only check values applicable to the model/modelstate rather than the entity we saved?
//The problem here is we may only have a few fields used in the viewmodel, but many in the entity
//so we could have a problem here with that.
}
filterContext.ExceptionHandled = true;
filterContext.Result = new ViewResult() { ViewData = filterContext.Controller.ViewData };
}
}

How to convert IEnumerable to Subsonic collection?

Embarrassing question really -- I have Subsonic collection, then I filter out some data using Where.
MyColl.Where(it => it.foo()==true)
now I would like to pass those data still as Subsonic collection. How to do it (the most properly way)? I checked constructor for Subsonic collection, ToX() methods, and googled.
edit: Subsonic 2.x series.
Thank you in advance, and sorry for naive question.
In SubSonic 2.x, the data isn't actually filtered out with Where() until the query is re-executed against the database. I'm assuming that's the same in 3.0. In 2.x there was a .Filter() method that would do what you are looking for.
The .Filter() method gets added to the generated classes. Here's what mine looks like (it's customized from the default):
/// <summary>
/// Filters an existing collection based on the set criteria. This is an in-memory filter.
/// All existing wheres are retained.
/// </summary>
/// <returns>TblSomethingOrOtherCollection</returns>
public TblSomethingOrOtherCollection Filter(SubSonic.Where w)
{
return Filter(w, false);
}
/// <summary>
/// Filters an existing collection based on the set criteria. This is an in-memory filter.
/// Existing wheres can be cleared if not needed.
/// </summary>
/// <returns>TblSomethingOrOtherCollection</returns>
public TblSomethingOrOtherCollection Filter(SubSonic.Where w, bool clearWheres)
{
if (clearWheres)
{
this.wheres.Clear();
}
this.wheres.Add(w);
return Filter();
}
/// <summary>
/// Filters an existing collection based on the set criteria. This is an in-memory filter.
/// Thanks to developingchris for this!
/// </summary>
/// <returns>TblSomethingOrOtherCollection</returns>
public TblSomethingOrOtherCollection Filter()
{
for (int i = this.Count - 1; i > -1; i--)
{
TblSomethingOrOther o = this[i];
foreach (SubSonic.Where w in this.wheres)
{
bool remove = false;
System.Reflection.PropertyInfo pi = o.GetType().GetProperty(w.ColumnName);
if (pi != null && pi.CanRead)
{
object val = pi.GetValue(o, null);
if (w.ParameterValue is Array)
{
Array paramValues = (Array)w.ParameterValue;
foreach (object arrayVal in paramValues)
{
remove = !Utility.IsMatch(w.Comparison, val, arrayVal);
if (remove)
break;
}
}
else
{
remove = !Utility.IsMatch(w.Comparison, val, w.ParameterValue);
}
}
if (remove)
{
this.Remove(o);
break;
}
}
}
return this;
}
}
For some reason i could never get the inline method of filter to work, but its easy to use like this:
SubSonic.Where w = new Where();
w.ColumnName = Product.CatIDColumn.PropertyName;
w.Comparison = Comparison.Equals;
w.ParameterValue = "1";
ProductCollection objFilteredCol = objProdCollection.Where(w).Filter();

VS2008 Windows Form Designer does not like my control

I have a control that is created like so:
public partial class MYControl : MyControlBase
{
public string InnerText {
get { return textBox1.Text; }
set { textBox1.Text = value; }
}
public MYControl()
{
InitializeComponent();
}
}
partial class MYControl
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.textBox1 = new System.Windows.Forms.TextBox();
this.listBox1 = new System.Windows.Forms.ListBox();
this.label1 = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// textBox1
//
this.textBox1.Location = new System.Drawing.Point(28, 61);
this.textBox1.Name = "textBox1";
this.textBox1.Size = new System.Drawing.Size(100, 20);
this.textBox1.TabIndex = 0;
//
// listBox1
//
this.listBox1.FormattingEnabled = true;
this.listBox1.Location = new System.Drawing.Point(7, 106);
this.listBox1.Name = "listBox1";
this.listBox1.Size = new System.Drawing.Size(120, 95);
this.listBox1.TabIndex = 1;
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(91, 42);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(35, 13);
this.label1.TabIndex = 2;
this.label1.Text = "label1";
//
// MYControl
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.label1);
this.Controls.Add(this.listBox1);
this.Controls.Add(this.textBox1);
this.Name = "MYControl";
this.Size = new System.Drawing.Size(135, 214);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label label1;
}
MyControlBase contains the definition for the ListBox and TextBox. Now when I try to view this control in the Form Designer it gives me these errors:
The variable 'listBox1' is either
undeclared or was never assigned.
The variable 'textBox1' is either
undeclared or was never assigned.
This is obviously wrong as they are defined in MyControlBase with public access. Is there any way to massage Form Designer into allowing me to visually edit my control?
I think you'll have to use base.listBox1 and base.textBox1. They are defined in MyControlBase which is the base class, not the child class where you need to use the this keyword.
Dunno if this is your problem, but the designer has trouble when multiple types are defined in the same .cs file. If this is the case, try using a .cs file for each class.
Sometimes (always?) VS needs you to recompile your project before it can successfully display your usercontrol in the designer.
Also take into account that the VS designer actually loads up and instantiates your control to show it on the form. Your code is actually running in the background. However it will not have all the things it might expect to be there - like some global application variables or even other things on the same form. Your control has to be prepared for the "design mode". Otherwise if it generates an exception the designer will not show it. There was a property on every control (don't remember the name, but you should find it easily) that allowed you to determine if the control is in "design mode" or actually running.
The compiler is right (as it tends to be).
Neither textbox1 nor listbox1 are defined in the source code. They don't appear in either the derived class or the base class.
You should add the following to your base class:
protected System.Windows.Forms.TextBox textbox1;
protected System.Windows.Forms.ListBox listbox1;
You'll also need to do the changes outlined by Nazgulled if you decide to use private instead of protected for textbox1 and listbox1.

Resources