Related
When creating a user like so:
using var trans = Db.BeginTransaction();
AuthRepository.CreateUserAuth(newUser, request.Password);
AuthRepository.AssignRoles(created, new List<string> { request.role });
//.. do other stuff
throw new Exception("other code may throw this");
trans.Commit();
The Auth repo has it's own connection so it's not part of the transaction. This means, if my code bails out, I end up with an unwanted user.
Is there any way to use a transaction with AuthRepository or is only way to manually write to user and role tables? I couldn't find an example of creating the password hash in docs when manually saving, is there any example?
You can't use the existing Auth Repository APIs within a transaction because each uses its own DB connection.
If you want you can take the implementation of those OrmLite Auth Respository APIs in OrmLiteAuthRepository.cs and move them into your method so they're all using the same DB Connection + Transaction.
Also note that to use Transactions in OrmLite you should use OpenTransaction() instead, e.g:
using (var dbTrans = db.OpenTransaction())
{
}
Adding class I used here in case it helps anyone as needed a little refactoring:
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using OutReachPete.ServiceModel.User;
using ServiceStack;
using ServiceStack.Auth;
using ServiceStack.OrmLite;
namespace OutReachPete.ServiceInterface.User
{
public class UserHelper
{
public static bool ForceCaseInsensitiveUserNameSearch { get; set; } = true;
public static bool UseDistinctRoleTables { get; set; } = true;
public static UserAuthCustom CreateUserAuth(UserAuthCustom newUser, string password, IDbConnection db)
{
newUser.ValidateNewUser(password);
AssertNoExistingUser(db, newUser);
newUser.PopulatePasswordHashes(password);
newUser.CreatedDate = DateTime.UtcNow;
newUser.ModifiedDate = newUser.CreatedDate;
db.Save(newUser);
newUser = db.SingleById<UserAuthCustom>(newUser.Id);
return newUser;
}
public static void AssertNoExistingUser(IDbConnection db, IUserAuth newUser,
IUserAuth exceptForExistingUser = null)
{
if (newUser.UserName != null)
{
var existingUser = GetUserAuthByUserName(db, newUser.UserName);
if (existingUser != null
&& (exceptForExistingUser == null || existingUser.Id != exceptForExistingUser.Id))
throw new ArgumentException(string.Format(ErrorMessages.UserAlreadyExistsTemplate1,
newUser.UserName.SafeInput()));
}
if (newUser.Email != null)
{
var existingUser = GetUserAuthByUserName(db, newUser.Email);
if (existingUser != null
&& (exceptForExistingUser == null || existingUser.Id != exceptForExistingUser.Id))
throw new ArgumentException(string.Format(ErrorMessages.EmailAlreadyExistsTemplate1,
newUser.Email.SafeInput()));
}
}
public static UserAuthCustom GetUserAuthByUserName(IDbConnection db, string userNameOrEmail)
{
var isEmail = userNameOrEmail.Contains("#");
var lowerUserName = userNameOrEmail.ToLower();
UserAuthCustom userAuth = null;
// Usernames/Emails are saved in Lower Case so we can do an exact search using lowerUserName
if (HostContext.GetPlugin<AuthFeature>()?.SaveUserNamesInLowerCase == true)
{
return isEmail
? db.Select<UserAuthCustom>(q => q.Email == lowerUserName).FirstOrDefault()
: db.Select<UserAuthCustom>(q => q.UserName == lowerUserName).FirstOrDefault();
}
// Try an exact search using index first
userAuth = isEmail
? db.Select<UserAuthCustom>(q => q.Email == userNameOrEmail).FirstOrDefault()
: db.Select<UserAuthCustom>(q => q.UserName == userNameOrEmail).FirstOrDefault();
if (userAuth != null)
return userAuth;
// Fallback to a non-index search if no exact match is found
if (ForceCaseInsensitiveUserNameSearch)
{
userAuth = isEmail
? db.Select<UserAuthCustom>(q => q.Email.ToLower() == lowerUserName).FirstOrDefault()
: db.Select<UserAuthCustom>(q => q.UserName.ToLower() == lowerUserName).FirstOrDefault();
}
return userAuth;
}
public static void AssignRoles(IAuthRepository userAuthRepo, IUserAuth userAuth, IDbConnection db,
ICollection<string> roles = null, ICollection<string> permissions = null)
{
if (userAuthRepo is IManageRoles managesRoles)
{
AssignRoles(userAuth.Id.ToString(), db, roles, permissions);
}
else
{
AssignRolesInternal(userAuth, roles, permissions);
SaveUserAuth(userAuth, db);
}
}
public static IUserAuth GetUserAuth(string userAuthId, IDbConnection db)
{
if (string.IsNullOrEmpty(userAuthId))
throw new ArgumentNullException(nameof(userAuthId));
return db.SingleById<UserAuthCustom>(int.Parse(userAuthId));
}
public static void AssignRoles(string userAuthId, IDbConnection db, ICollection<string> roles = null, ICollection<string> permissions = null)
{
var userAuth = GetUserAuth(userAuthId, db);
if (!UseDistinctRoleTables)
{
if (!roles.IsEmpty())
{
foreach (var missingRole in roles.Where(x => userAuth.Roles == null || !userAuth.Roles.Contains(x)))
{
if (userAuth.Roles == null)
userAuth.Roles = new List<string>();
userAuth.Roles.Add(missingRole);
}
}
if (!permissions.IsEmpty())
{
foreach (var missingPermission in permissions.Where(x => userAuth.Permissions == null || !userAuth.Permissions.Contains(x)))
{
if (userAuth.Permissions == null)
userAuth.Permissions = new List<string>();
userAuth.Permissions.Add(missingPermission);
}
}
SaveUserAuth(userAuth, db);
}
else
{
var now = DateTime.UtcNow;
var userRoles = db.Select<UserAuthRole>(q => q.UserAuthId == userAuth.Id);
if (!roles.IsEmpty())
{
var roleSet = userRoles.Where(x => x.Role != null).Select(x => x.Role).ToHashSet();
foreach (var role in roles)
{
if (!roleSet.Contains(role))
{
db.Insert(new UserAuthRole
{
UserAuthId = userAuth.Id,
Role = role,
CreatedDate = now,
ModifiedDate = now,
});
}
}
}
if (!permissions.IsEmpty())
{
var permissionSet = userRoles.Where(x => x.Permission != null).Select(x => x.Permission).ToHashSet();
foreach (var permission in permissions)
{
if (!permissionSet.Contains(permission))
{
db.Insert(new UserAuthRole
{
UserAuthId = userAuth.Id,
Permission = permission,
CreatedDate = now,
ModifiedDate = now,
});
}
}
}
}
}
private static void AssignRolesInternal(IUserAuth userAuth, ICollection<string> roles, ICollection<string> permissions)
{
if (!roles.IsEmpty())
{
foreach (var missingRole in roles.Where(x => userAuth.Roles == null || !userAuth.Roles.Contains(x)))
{
if (userAuth.Roles == null)
userAuth.Roles = new List<string>();
userAuth.Roles.Add(missingRole);
}
}
if (!permissions.IsEmpty())
{
foreach (var missingPermission in permissions.Where(x =>
userAuth.Permissions == null || !userAuth.Permissions.Contains(x)))
{
if (userAuth.Permissions == null)
userAuth.Permissions = new List<string>();
userAuth.Permissions.Add(missingPermission);
}
}
}
public static void SaveUserAuth(IUserAuth userAuth, IDbConnection db)
{
if (userAuth == null)
throw new ArgumentNullException(nameof(userAuth));
userAuth.ModifiedDate = DateTime.UtcNow;
if (userAuth.CreatedDate == default(DateTime))
userAuth.CreatedDate = userAuth.ModifiedDate;
db.Save((UserAuthCustom)userAuth);
}
}
}
Then just call it like:
var created = UserHelper.CreateUserAuth(newUser, request.Password, Db);
UserHelper.AssignRoles(AuthRepository, created, Db, new List<string> { request.CreateUserType.ToString() });
It will only use the connection passed. Just change UserAuthCustom to whatever you user class is.
I have the following issue using ASP.NET Core 2.2 and Automapper 9.0.0:
I have a Entity that I map to a dto, this works fine. Inside that entity are a few entities as well. These cannot be flattened as they are also needed by our client. There is however one property within those nested entities that need additional mapping.
within the Profile class of the overlapping Entity i do this:
public class OperationalToPalletResultProfile : Profile
{
public OperationalToPalletResultProfile()
{
string lang = null;
CreateMap<TblDatOperationalHUTrace, PalletResult>()
.ForMember(dest => dest.TransCode, act => act.MapFrom((src, _, transactionCode, ctx) =>
ctx.Mapper.Map<TblLstCode, TransactionCodeResult>(src.TransCode)));
}
}
This effectively maps the TblLstCode to TransactionCodeResult correctly. However the "DisplayDesc" property of the TransactionCodeResult remains null...
Note: the "lang" property gets set with the "ProjectTo" method:
var values = await query
.Take(5000)
.ProjectTo<PalletResult>(mapper.ConfigurationProvider, new { lang = language })
.ToListAsync()
.ConfigureAwait(false);
I have tried:
1- Using "AfterMap" in Profile class (result: DisplayDesc = null)
CreateMap<TblDatOperationalHUTrace, PalletResult>()
.ForMember(dest => dest.TransCode, act => act.MapFrom((src, _, transactionCode, ctx) =>
ctx.Mapper.Map<TblLstCode, TransactionCodeResult>(src.TransCode, opt =>
opt.AfterMap((source, destination) =>
destination.DisplayDesc = $"[{destination.ShortName}] {destination.DescToLanguageDesc(destination, lang)}"))))
Note: DescToLanguageDesc is a function that finds a property in the entity with reflection (this works and is not part of the problem)
2- Creating an IMappingAction to use in the Profile of the SubEntity (result: DisplayDesc = null)
public class TransactionCodeTranslation : IMappingAction<TblLstCode, TransactionCodeResult>
{
private readonly IHttpContextAccessor httpContextAccessor;
public TransactionCodeTranslation(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public void Process(TblLstCode source, TransactionCodeResult destination, ResolutionContext context)
{
//Get language from httpContext - this works correctly
destination.DisplayDesc = $"[{source.ShortName}] {source.DescToLanguageDesc(source, language)}";
}
}
public class TransactionCodeProfile : Profile
{
public TransactionCodeProfile()
{
string language = string.Empty;
CreateMap<TblLstCode, TransactionCodeResult>()
.AfterMap<TransactionCodeTranslation>();
}
}
This doesn't work. HOWEVER if I use option 2 directly:
var tc = await codeRepo.TableNoTracking.FirstOrDefaultAsync(x => x.ShortName == "[something]").ConfigureAwait(false);
var transactionCode = mapper.Map<TblLstCode, TransactionCodeResult>(tc);
Then it works correctly! But that would mean i would have to loop my result and map every object in the result again...
Is there a way to do it like option 1?
Thank you!
Edit 1:
Per Lucian Bargaoanu's request i added a BuildExecutionPlan:
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<TblDatOperationalHUTrace, PalletResult>()
.ForMember(dest => dest.TransCode, act => act.MapFrom((src, _, transactionCode, ctx) =>
ctx.Mapper.Map<TblLstCode, TransactionCodeResult>(src.TransCode, opt =>
opt.AfterMap((source, destination) =>
destination.DisplayDesc = $"[{destination.ShortName}] {destination.DescToLanguageDesc(destination, language)}"))));
cfg.CreateMap<TblLstCode, TransactionCodeResult>()
.AfterMap<TransactionCodeTranslation>();
});
var expression = config.BuildExecutionPlan(typeof(TblDatOperationalHUTrace), typeof(PalletResult));
var expression2 = config.BuildExecutionPlan(typeof(TblLstCode), typeof(TransactionCodeResult));
Result of expression:
(src, dest, ctxt) =>
{
PalletResult typeMapDestination;
return (src == null)
? null
: {
typeMapDestination = dest ?? new PalletResult();
try
{
var resolvedValue = mappingFunction.Invoke(
src,
typeMapDestination,
typeMapDestination.TransCode,
ctxt);
var propertyValue = (resolvedValue == null) ? null : resolvedValue;
typeMapDestination.TransCode = propertyValue;
}
catch (Exception ex)
{
throw new AutoMapperMappingException(
"Error mapping types.",
ex,
AutoMapper.TypePair,
TypeMap,
PropertyMap);
return null;
}
return typeMapDestination;
};
}
Result Expression2:
(src, dest, ctxt) =>
{
TransactionCodeResult typeMapDestination;
return (src == null)
? null
: {
typeMapDestination = dest ?? new TransactionCodeResult();
try
{
var resolvedValue = ((src == null) || false) ? default(int) : src.Id;
typeMapDestination.Id = resolvedValue;
}
catch (Exception ex)
{
throw new AutoMapperMappingException(
"Error mapping types.",
ex,
AutoMapper.TypePair,
TypeMap,
PropertyMap);
return default(int);
}
try
{
var resolvedValue = ((src == null) || false) ? null : src.ShortName;
var propertyValue = (resolvedValue == null) ? null : resolvedValue;
typeMapDestination.ShortName = propertyValue;
}
catch (Exception ex)
{
throw new AutoMapperMappingException(
"Error mapping types.",
ex,
AutoMapper.TypePair,
TypeMap,
PropertyMap);
return null;
}
try
{
var resolvedValue = ((src == null) || false) ? default(int) : src.CodeTypeId;
typeMapDestination.CodeTypeId = resolvedValue;
}
catch (Exception ex)
{
throw new AutoMapperMappingException(
"Error mapping types.",
ex,
AutoMapper.TypePair,
TypeMap,
PropertyMap);
return default(int);
}
try
{
var resolvedValue = ((src == null) || false) ? null : src.DescLC;
var propertyValue = (resolvedValue == null) ? null : resolvedValue;
typeMapDestination.DescLC = propertyValue;
}
catch (Exception ex)
{
throw new AutoMapperMappingException(
"Error mapping types.",
ex,
AutoMapper.TypePair,
TypeMap,
PropertyMap);
return null;
}
try
{
var resolvedValue = ((src == null) || false) ? null : src.DescEN;
var propertyValue = (resolvedValue == null) ? null : resolvedValue;
typeMapDestination.DescEN = propertyValue;
}
catch (Exception ex)
{
throw new AutoMapperMappingException(
"Error mapping types.",
ex,
AutoMapper.TypePair,
TypeMap,
PropertyMap);
return null;
}
try
{
var resolvedValue = ((src == null) || false) ? null : src.DescFR;
var propertyValue = (resolvedValue == null) ? null : resolvedValue;
typeMapDestination.DescFR = propertyValue;
}
catch (Exception ex)
{
throw new AutoMapperMappingException(
"Error mapping types.",
ex,
AutoMapper.TypePair,
TypeMap,
PropertyMap);
return null;
}
try
{
var resolvedValue = ((src == null) || false) ? null : src.DescGE;
var propertyValue = (resolvedValue == null) ? null : resolvedValue;
typeMapDestination.DescGE = propertyValue;
}
catch (Exception ex)
{
throw new AutoMapperMappingException(
"Error mapping types.",
ex,
AutoMapper.TypePair,
TypeMap,
PropertyMap);
return null;
}
try
{
var resolvedValue = ((src == null) || false) ? null : src.DescNL;
var propertyValue = (resolvedValue == null) ? null : resolvedValue;
typeMapDestination.DescNL = propertyValue;
}
catch (Exception ex)
{
throw new AutoMapperMappingException(
"Error mapping types.",
ex,
AutoMapper.TypePair,
TypeMap,
PropertyMap);
return null;
}
afterFunction.Invoke(src, typeMapDestination, ctxt);
return typeMapDestination;
};
}
EDIT 2:
Ok, I actually found the solution. I was overcomplicating things for no reason...
I changed OperationalToPalletResultProfile to:
public class OperationalToPalletResultProfile : Profile
{
public OperationalToPalletResultProfile()
{
CreateMap<TblDatOperationalHUTrace, PalletResult>();
}
}
The TransactionCodeProfile remained the same:
public class TransactionCodeProfile : Profile
{
public TransactionCodeProfile()
{
CreateMap<TblLstCode, TransactionCodeResult>()
.AfterMap<TransactionCodeTranslation>();
}
}
And then where i use my mapping i changed this:
//results as TblDatOperationalHUTrace
var queryresult = await query
.Take(5000)
//.ProjectTo<PalletResult>(mapper.ConfigurationProvider) Don't do the mapping here anymore
.ToListAsync()
.ConfigureAwait(false);
//Map results to PalletResult
var values = mapper.Map<List<TblDatOperationalHUTrace>, List<PalletResult>>(queryresult); //Do the mapping here
Ok i actually found the solution myself:
I changed OperationalToPalletResultProfile to:
public class OperationalToPalletResultProfile : Profile
{
public OperationalToPalletResultProfile()
{
CreateMap<TblDatOperationalHUTrace, PalletResult>();
}
}
The TransactionCodeProfile remained the same:
public class TransactionCodeProfile : Profile
{
public TransactionCodeProfile()
{
CreateMap<TblLstCode, TransactionCodeResult>()
.AfterMap<TransactionCodeTranslation>();
}
}
And then where i use my mapping i changed this:
//results as TblDatOperationalHUTrace
var queryresult = await query
.Take(5000)
//.ProjectTo<PalletResult>(mapper.ConfigurationProvider) Don't do the mapping here anymore
.ToListAsync()
.ConfigureAwait(false);
//Map results to PalletResult
var values = mapper.Map<List<TblDatOperationalHUTrace>, List<PalletResult>>(queryresult); //Do the mapping here
And then it works like it's supposed to.
I need to be able to send an email to the original requester when a PO is created from a Requisition in Acumatica 6.1.
Per Acumatica, the Notification screen cannot handle this functionality, so I have this code to extend the POOrder Entry graph, which sends an email to the Customer's contact email from the Requisition when a PO is created (along with the RQRequisitionEntryExt trigger):
public class POOrderEntryExt : PXGraphExtension<POOrderEntry>
{
private bool sendEmailNotification = false;
public bool SendEmailNotification
{
get
{
return sendEmailNotification;
}
set
{
sendEmailNotification = value;
}
}
[PXOverride]
public void Persist(Action del)
{
using (var ts = new PXTransactionScope())
{
if (del != null)
{
del();
}
if (SendEmailNotification)
{
bool sent = false;
string sError = "Failed to send E-mail.";
try
{
Notification rowNotification = PXSelect<Notification,
Where<Notification.name, Equal<Required<Notification.name>>>>
.Select(Base, "PurchaseOrderNotification");
if (rowNotification == null)
throw new PXException("Notification Template was not found.");
var order = Base.Document.Current;
var requisition = (RQRequisition)PXSelect<RQRequisition,
Where<RQRequisition.reqNbr, Equal<Current<POOrder.rQReqNbr>>>>
.SelectSingleBound(Base, new object[] { order });
if (requisition.CustomerID != null)
{
var customer = (BAccountR)PXSelectorAttribute.Select<RQRequisition.customerID>(
Base.Caches[typeof(RQRequisition)], requisition);
if (customer != null)
{
var defCustContact = (Contact)PXSelectorAttribute.Select<BAccountR.defContactID>(
Base.Caches[typeof(BAccountR)], customer);
if (String.IsNullOrEmpty(defCustContact.EMail))
throw new PXException("E-mail is not specified for Customer Contact.");
var sender = TemplateNotificationGenerator.Create(order,
rowNotification.NotificationID.Value);
sender.RefNoteID = order.NoteID;
sender.MailAccountId = rowNotification.NFrom.HasValue ?
rowNotification.NFrom.Value :
PX.Data.EP.MailAccountManager.DefaultMailAccountID;
sender.To = defCustContact.EMail;
sent |= sender.Send().Any();
}
}
}
catch (Exception Err)
{
sent = false;
sError = Err.Message;
}
if (!sent)
throw new PXException(sError);
}
ts.Complete();
}
}
}
And this to modify RQRequisitionEntry:
public class RQRequisitionEntryExt : PXGraphExtension<RQRequisitionEntry>
{
public PXAction<RQRequisition> createPOOrder;
[PXButton(ImageKey = PX.Web.UI.Sprite.Main.DataEntry)]
[PXUIField(DisplayName = Messages.CreateOrders)]
public IEnumerable CreatePOOrder(PXAdapter adapter)
{
PXGraph.InstanceCreated.AddHandler<POOrderEntry>((graph) =>
{
graph.GetExtension<POOrderEntryExt>().SendEmailNotification = true;
});
return Base.createPOOrder.Press(adapter);
}
}
In order to send an email to the Requester's (Employee) contact email from the Request, I modified the POOrderEntryExt to pull the information from the Request object and the Employee's Contact email (I left the RQRequisitionEntryExt the same and in place):
public class POOrderEntryExt : PXGraphExtension<POOrderEntry>
{
private bool sendEmailNotification = false;
public bool SendEmailNotification
{
get
{
return sendEmailNotification;
}
set
{
sendEmailNotification = value;
}
}
[PXOverride]
public void Persist(Action del)
{
using (var ts = new PXTransactionScope())
{
if (del != null)
{
del();
}
if (SendEmailNotification)
{
bool sent = false;
string sError = "Failed to send E-mail.";
try
{
Notification rowNotification = PXSelect<Notification,
Where<Notification.name, Equal<Required<Notification.name>>>>
.Select(Base, "PurchaseOrderNotification");
if (rowNotification == null)
throw new PXException("Notification Template was not found.");
var order = Base.Document.Current;
var requisition = (RQRequisition)PXSelect<RQRequisition,
Where<RQRequisition.reqNbr, Equal<Current<POOrder.rQReqNbr>>>>
.SelectSingleBound(Base, new object[] { order });
var request = (RQRequest)PXSelectJoin<RQRequest,
InnerJoin<RQRequisitionContent,
On<RQRequisitionContent.orderNbr, Equal<RQRequest.orderNbr>>>,
Where<RQRequisitionContent.reqNbr, Equal<POOrder.rQReqNbr>>>
.SelectSingleBound(Base, new object[] { order });
if (request.EmployeeID != null)
{
var employee = (BAccountR)PXSelectorAttribute.Select<RQRequest.employeeID>(
Base.Caches[typeof(RQRequest)], request);
if (employee != null)
{
var defEmpContact = (Contact)PXSelectorAttribute.Select<BAccountR.defContactID>(
Base.Caches[typeof(BAccountR)], employee);
if (String.IsNullOrEmpty(defEmpContact.EMail))
throw new PXException("E-mail is not specified for Employee Contact.");
var sender = TemplateNotificationGenerator.Create(order,
rowNotification.NotificationID.Value);
sender.RefNoteID = order.NoteID;
sender.MailAccountId = rowNotification.NFrom.HasValue ?
rowNotification.NFrom.Value :
PX.Data.EP.MailAccountManager.DefaultMailAccountID;
sender.To = defEmpContact.EMail;
sent |= sender.Send().Any();
}
else
throw new PXException("Customer not found.");
}
else
throw new PXException("Request not found.");
}
catch (Exception Err)
{
sent = false;
sError = Err.Message;
}
if (!sent)
throw new PXException(sError);
}
ts.Complete();
}
}
}
I can get the original code to send an email in my development environment, but my modified code only returns the outer "Failed to send E-mail" error.
Can anyone help point me in the right direction to get my modifications to work?
Because in Acumatica there is one-to-many relationship between RQRequisition and RQRequest, I believe the better approach is to loop through all requests linked to the current requisition and compose a list of requester's emails. After that we can go ahead and send emails to all requesters as part of the Create Orders operation:
public class POOrderEntryExt : PXGraphExtension<POOrderEntry>
{
private bool sendEmailNotification = false;
public bool SendEmailNotification
{
get
{
return sendEmailNotification;
}
set
{
sendEmailNotification = value;
}
}
[PXOverride]
public void Persist(Action del)
{
using (var ts = new PXTransactionScope())
{
if (del != null)
{
del();
}
if (SendEmailNotification)
{
bool sent = false;
string sError = "Failed to send E-mail.";
try
{
Notification rowNotification = PXSelect<Notification,
Where<Notification.name, Equal<Required<Notification.name>>>>
.Select(Base, "PONotification");
if (rowNotification == null)
throw new PXException("Notification Template was not found.");
var order = Base.Document.Current;
var emails = new List<string>();
var requests = PXSelectJoinGroupBy<RQRequest,
InnerJoin<RQRequisitionContent,
On<RQRequest.orderNbr,
Equal<RQRequisitionContent.orderNbr>>>,
Where<RQRequisitionContent.reqNbr,
Equal<Required<RQRequisition.reqNbr>>>,
Aggregate<GroupBy<RQRequest.orderNbr>>>
.Select(Base, order.RQReqNbr);
foreach (RQRequest request in requests)
{
if (request.EmployeeID != null)
{
var requestCache = Base.Caches[typeof(RQRequest)];
requestCache.Current = request;
var emplOrCust = (BAccountR)PXSelectorAttribute
.Select<RQRequest.employeeID>(requestCache, request);
if (emplOrCust != null)
{
var defEmpContact = (Contact)PXSelectorAttribute
.Select<BAccountR.defContactID>(
Base.Caches[typeof(BAccountR)], emplOrCust);
if (!String.IsNullOrEmpty(defEmpContact.EMail) &&
!emails.Contains(defEmpContact.EMail))
{
emails.Add(defEmpContact.EMail);
}
}
}
}
foreach (string email in emails)
{
var sender = TemplateNotificationGenerator.Create(order,
rowNotification.NotificationID.Value);
sender.RefNoteID = order.NoteID;
sender.MailAccountId = rowNotification.NFrom.HasValue ?
rowNotification.NFrom.Value :
PX.Data.EP.MailAccountManager.DefaultMailAccountID;
sender.To = email;
sent |= sender.Send().Any();
}
}
catch (Exception Err)
{
sent = false;
sError = Err.Message;
}
if (!sent)
throw new PXException(sError);
}
ts.Complete();
}
}
}
I have written quite a few different add-ins now but I keep struggling to get a windows form working on Revit. The program builds fine and I have the dll set up for Revit to access.
Here are the different sections of my code. The program is more extensive than what is seen but I believe that the problem is a reference issue or a problem with my ADDIN file. Maybe there is a different way I need to set up my ADDIN file since I have a windows form in it?? Let me know.
Here is a Dropbox folder with the screenshots in it.
Let me know if there is anything else you need to see. The error in Revit says it has to do with the FullName but I believe I put it in the ADDIN file correctly, and I did it the same as I had for other ADDINs.
Thank you for your help!
[TransactionAttribute(TransactionMode.Manual)]
[RegenerationAttribute(RegenerationOption.Manual)]
public class CicuitChecker : IExternalCommand
{
public Result Execute(
ExternalCommandData commandData,
ref string message,
ElementSet elements)
{
//set document variable
Document document = commandData.Application.ActiveUIDocument.Document;
using (Transaction trans = new Transaction(document))
{
trans.Start("Circuit Checker");
UIApplication uiApp = commandData.Application;
Document doc = uiApp.ActiveUIDocument.Document;
//run through looped form in case of user not selecting needed fields, and store what family the user wants the program to check
Boolean messedUp = false;
Boolean All = false, lightF = false, recep = false, elecEquip = false, equipCon = false, junc = false, panels = false;
FilteredElementCollector collector = new FilteredElementCollector(doc), collector2 = new FilteredElementCollector(doc);
while (messedUp)
{
CircuitChecker.CircuitCheckerForm form = new CircuitChecker.CircuitCheckerForm();
form.ShowDialog();
//Get application and document objects
foreach (String item in form.getSelectionElementsLB())
{
if (item.Equals("All"))
{
All = true;
break;
}
else if (item.Equals("Lighting Fixtures"))
{
lightF = true;
}
else if (item.Equals("Recepticales"))
{
recep = true;
}
else if (item.Equals("Electrical Equipment (including Panels)"))
{
elecEquip = true;
}
else if (item.Equals("Junctions"))
{
junc = true;
}
else
{
messedUp = true;
TaskDialog.Show("Error", "At least one element must be selected.");
}
}
if (form.getSelectionPlaceLB().Equals("Entire Project"))
{
collector
= new FilteredElementCollector(doc)
.WhereElementIsNotElementType();
collector2
= new FilteredElementCollector(doc)
.WhereElementIsNotElementType();
}
else if (form.getSelectionPlaceLB().Equals("Elements in Current View"))
{
collector
= new FilteredElementCollector(doc, document.ActiveView.Id)
.WhereElementIsNotElementType();
collector2
= new FilteredElementCollector(doc, document.ActiveView.Id)
.WhereElementIsNotElementType();
}
else
{
messedUp = true;
TaskDialog.Show("Error", "A place must be selected.");
}
}
Color color = new Color(138, 43, 226); // RGB
OverrideGraphicSettings ogs = new OverrideGraphicSettings();
OverrideGraphicSettings ogsOriginal = new OverrideGraphicSettings();
ogs.SetProjectionLineColor(color);
int notCircuited = 0;
//ElementId symbolId = family
ElementCategoryFilter lightFilter = new ElementCategoryFilter(BuiltInCategory.OST_LightingFixtures);
ElementCategoryFilter recepFilter = new ElementCategoryFilter(BuiltInCategory.OST_ElectricalFixtures);
ElementCategoryFilter elecEquipFilter = new ElementCategoryFilter(BuiltInCategory.OST_ElectricalEquipment);
//ElementClassFilter filter = new ElementClassFilter(typeof("Junction Boxes - Load"));
//FamilyInstanceFilter juncFilter1 = new FamilyInstanceFilter(doc, );
LogicalOrFilter first = new LogicalOrFilter(lightFilter, recepFilter);
if (All)
{
collector.WherePasses(first);
IList<Element> allArr = collector.ToElements();
foreach (Element e in allArr)
{
int cirNum = e.get_Parameter(BuiltInParameter.RBS_ELEC_CIRCUIT_NUMBER).AsInteger();
String panel = e.get_Parameter(BuiltInParameter.RBS_ELEC_CIRCUIT_PANEL_PARAM).AsString();
if (!(IsNumeric(cirNum)) || (panel.Equals("")) || (panel.Equals("<unnamed>")))
{
doc.ActiveView.SetElementOverrides(e.Id, ogs);
notCircuited++;
}
else
{
doc.ActiveView.SetElementOverrides(e.Id, ogsOriginal);
}
}
collector2.WherePasses(elecEquipFilter);
IList<Element> elecEquipArr = collector.ToElements();
foreach (Element e in elecEquipArr)
{
String panel = e.get_Parameter(BuiltInParameter.RBS_ELEC_PANEL_SUPPLY_FROM_PARAM).AsString();
if ((panel.Equals("")))
{
doc.ActiveView.SetElementOverrides(e.Id, ogs);
notCircuited++;
}
else
{
doc.ActiveView.SetElementOverrides(e.Id, ogsOriginal);
}
}
TaskDialog.Show("Circuit Checker", notCircuited + " lighting fixtures are not circuited in this view.");
trans.Commit();
}
if (!trans.HasEnded())
{
if (lightF)
{
collector.WherePasses(lightFilter);
IList<Element> lightArr = collector.ToElements();
foreach (Element e in lightArr)
{
int cirNum = e.get_Parameter(BuiltInParameter.RBS_ELEC_CIRCUIT_NUMBER).AsInteger();
String panel = e.get_Parameter(BuiltInParameter.RBS_ELEC_CIRCUIT_PANEL_PARAM).AsString();
if (!(IsNumeric(cirNum)) || (panel.Equals("")) || (panel.Equals("<unnamed>")))
{
doc.ActiveView.SetElementOverrides(e.Id, ogs);
notCircuited++;
}
else
{
doc.ActiveView.SetElementOverrides(e.Id, ogsOriginal);
}
}
}
if (recep)
{
collector.WherePasses(recepFilter);
IList<Element> recepArr = collector.ToElements();
foreach (Element e in recepArr)
{
int cirNum = e.get_Parameter(BuiltInParameter.RBS_ELEC_CIRCUIT_NUMBER).AsInteger();
String panel = e.get_Parameter(BuiltInParameter.RBS_ELEC_CIRCUIT_PANEL_PARAM).AsString();
if (!(IsNumeric(cirNum)) || (panel.Equals("")) || (panel.Equals("<unnamed>")))
{
doc.ActiveView.SetElementOverrides(e.Id, ogs);
notCircuited++;
}
else
{
doc.ActiveView.SetElementOverrides(e.Id, ogsOriginal);
}
}
}
if (elecEquip)
{
collector.WherePasses(elecEquipFilter);
IList<Element> elecEquipArr = collector.ToElements();
foreach (Element e in elecEquipArr)
{
String panel = e.get_Parameter(BuiltInParameter.RBS_ELEC_PANEL_SUPPLY_FROM_PARAM).AsString();
if ((panel.Equals("")))
{
doc.ActiveView.SetElementOverrides(e.Id, ogs);
notCircuited++;
}
else
{
doc.ActiveView.SetElementOverrides(e.Id, ogsOriginal);
}
}
}
if (junc)
{
collector.WherePasses(recepFilter);
IList<Element> juncArr = collector.ToElements();
foreach (Element e in juncArr)
{
int cirNum = e.get_Parameter(BuiltInParameter.RBS_ELEC_CIRCUIT_NUMBER).AsInteger();
String panel = e.get_Parameter(BuiltInParameter.RBS_ELEC_CIRCUIT_PANEL_PARAM).AsString();
if (!(IsNumeric(cirNum)) || (panel.Equals("")) || (panel.Equals("<unnamed>")))
{
doc.ActiveView.SetElementOverrides(e.Id, ogs);
notCircuited++;
}
else
{
doc.ActiveView.SetElementOverrides(e.Id, ogsOriginal);
}
}
}
TaskDialog.Show("Circuit Checker", notCircuited + " lighting fixtures are not circuited in this view.");
trans.Commit();
}
}
return Result.Succeeded;
}
public static Boolean IsNumeric(Object Expression)
{
if (Expression == null || Expression is DateTime)
return false;
if (Expression is Int16 || Expression is Int32 || Expression is Int64 || Expression is Decimal || Expression is Single || Expression is Double || Expression is Boolean)
return true;
try
{
if (Expression is string)
Double.Parse(Expression as string);
else
Double.Parse(Expression.ToString());
return true;
}
catch { } // just dismiss errors but return false
return false;
}
}
This code is having the functionality in the 'main class.' I have since moved the functionality to the form class as konrad suggested but am still receiving the FullClassName error in Revit. Please Help!
The schedule data add-in provides a full Visual Studio solution demonstrating how to display a Windows form in a Revit add-in, including the generation of the Windows form on the fly:
http://thebuildingcoder.typepad.com/blog/2012/05/the-schedule-api-and-access-to-schedule-data.html
Here's how I usually set up my Windows Forms based External Commands. Remember that you have to create an External Command, and your addin manifest must point at this class. Then from this class you can launch the Form like so:
[Transaction(TransactionMode.Manual)]
public class SomeCommand : IExternalCommand
{
public Result Execute(
ExternalCommandData commandData,
ref string message,
ElementSet elements)
{
// Get application and document objects
UIApplication uiApp = commandData.Application;
Document doc = uiApp.ActiveUIDocument.Document;
UIDocument uidoc = uiApp.ActiveUIDocument;
try
{
SomeNamespace.SomeForm form = new SomeNamespace.SomeForm(doc);
form.ShowDialog();
return Result.Succeeded;
}
// Catch any exceptions and display them
catch (Autodesk.Revit.Exceptions.OperationCanceledException)
{
return Result.Cancelled;
}
catch (Exception ex)
{
message = ex.Message;
return Result.Failed;
}
}
}
So I have a Form class that I instantiate from my ExternalCommand and pass Document to its constructor. That way I have access to document when I am interacting with the form later. I wire up all functionality in code behind of the Form.
Agree, the OP's question is why doesn't the addin work...
From looking at the images, it seems like the issue is that Revit is not properly finding the full class name of the command.
It's a little unusual that you don't have your command class wrapped in a namespace (your form class is, for example).
I would recommend wrapping it in a namespace like "circuitchecker" - like your form class.
Then the "full name" in the addin file would become "circuitchecker.circuitchecker"
(the namespace.classname) - this helps Revit distinguish different classes that might have the same name.
side note: I don't believe that putting a URL into the Image/LargeImage fields in the addin will work - but not positive.
Can someone please shed some light on why this CSJS code is always running my SSJS code regardless of the CSJS seeming to return true or false?
CSJS code
message = "Starting Settle ATM Process..."
$(".infoMessage").text(message);
atmID = $("input.atmID").val(); //pull from hidden input
var deferred = atmRPC.closeATMFirstSettle();
deferred.addCallback(function(result){
if (result == "false") {
alert("Close command failed during 1st Settle ATM process. System will retry...");
var insideDeferred = atmMRPC.closeATMSecondSettle();
insideDeferred.addCallback(function(result) {
if (result == "false") {
message = "Close command failed during Settle ATM process. Please try again later."
$(".infoMessage").text(message);
atmRPC.updateInfoMsg(message);
return false;
} else if(result == "true"){
var deferred = atmRPC.settleATMFirst();
deferred.addCallback(function(result){
if (result == "false") {
alert("Settlement failed during 1st attempt. System will retry...");
var insideDeferred = atmMRPC.settleATMSecond();
insideDeferred.addCallback(function(result) {
if (result == "false") {
message = "Settlement failed. Please try again later."
$(".infoMessage").text(message);
atmRPC.updateInfoMsg(message);
return false;
} else if(result == "true"){
message = atmID + " has been successfully Settled."
$(".infoMessage").text(message);
atmRPC.updateInfoMsg(message);
// return true;
}
})
} else if(result == "true"){
message = atmID + " has been successfully Settled."
$(".infoMessage").text(message);
atmRPC.updateInfoMsg(message);
// return true;
}
})
}
})
} else if(result == "true"){
var deferred = atmRPC.settleATMFirst();
deferred.addCallback(function(result){
if (result == "false") {
alert("Settlement failed during 1st attempt. System will retry...");
var insideDeferred = atmMRPC.settleATMSecond();
insideDeferred.addCallback(function(result) {
if (result == "false") {
alert("Settlement failed during 1st attempt. System will retry...");
message = "Settlement failed. Please try again later."
$(".infoMessage").text(message);
atmRPC.updateInfoMsg(message);
return false;
} else if(result == "true"){
message = atmID + " has been successfully Settled."
$(".infoMessage").text(message);
atmRPC.updateInfoMsg(message);
// return true;
}
})
} else if(result == "true"){
message = atmID + " has been successfully Settled."
$(".infoMessage").text(message);
atmRPC.updateInfoMsg(message);
// return true;
}
})
}
})
closeATMFirsSettle and closeATMSecondSettle are identical with exception of the boolean variable
if(atmBean.atmStatus == "OPEN" || atmBean.atmStatus == "WOUNDED") {
var firstTry:boolean = atmBean.closeATM(atmBean.atmID, userBean.userID);
return firstTry.toString();
} else if(atmBean.atmStatus == "CLOSED") {
return "true";
} else {
return "false";
}
settleATMFirst and settleATMSecond are identical with exception of the boolean variable
sessionScope.infoMsg = null;
try {
if(atmBean.atmAmountReceived == null) {
var settleAmt:Integer = Integer.parseInt(atmBean.atmTodaySettlementAmt);
} else {
var settleAmt:Integer = Integer.parseInt(atmBean.atmAmountReceived);
}
} catch(e) {
message:String = "1st Settle ATM Amount Error: " + e;
logInfo(message);
return sessionScope.infoMsg = message;
}
var firstTry:boolean = atmBean.settleATM(atmBean.atmID, userBean.userID, settleAmt);
return firstTry.toString();
atmBean.settleATM
public boolean settleATM(String atmId, String employeeId, Integer settlementAmount, String attempt){
//This method called by RPC control from client JS
AtmOperationSettlementResult result = null;
boolean returnValue = false;
try {
result = AtmTerminalUtil.settleAtm(atmID, employeeId, settlementAmount);
returnValue = result.getSuccess();
if(attempt.equals("Second")) {
System.out.println("ATM Bean - Second Attempt");
if(!returnValue) {
handleATMError(result);
}
}
} catch(Exception e) {
System.out.println(e);
}
//saveATMHistory();
return returnValue; //return status of operation to UI
}
I know the SSJS in the Server tab is always being ran because the log entries show the value of a viewScope that is set if the atmBean.settleATM returns a false.
Sorry, didn't realize until the last comment that everyone may be thinking the SSJS code I'm talking about is in the RPC, but I'm referring to the Server tab.