How to enable Security Logging AX2012 (SecurityTasks to SecurityRoles) - axapta

I want to log SecurityRole - SecurityTask changes, and since the EeUserRoleChangeLog logs User – SecurityRole changes (for Estonia, but commenting out language detection, it logs also for us here in Holland perfectly). I think that basis can be used to log those changes.
So I first created the table:
EeRoleTaskChangeLog with the following fields:
AddRemove EnumType: AddRemove
ChangedBy EDT: UserID
SecurityRole EDT: RefRecID
SecurityTask EDT: RefRecId
I then noticed that within the class SysSecRole, User-Role changes logging is triggered within method removeFromSelectedUser. So whenever someone changes something from the AOT or the front-end, all these changes are then logged within the EeUserRoleChangeLog.
So I extended SysSecTask with the method removeFromSelectedRole:
public void removeFromSelectedRole()
{
RefRecId SecurityRole;
SysSecTreeTasks taskTree;
SecurityRoleTaskGrant SecurityRoleTaskGrant;
taskTree = tree as SysSecTreeTasks;
securityRole = taskTree.getSecurityRole();
ttsbegin;
EePersonalDataAccessLogging::logRoleTaskChange(recId, 0, securityRole, AddRemove::Remove);
delete_from securityRoleTaskGrant where securityRoleTaskGrant.SecurityRole == securityRole && securityRoleTaskGrant.SecurityTask == recId;
ttscommit;
if (treeControl)
{
tree.deleteTreeItem(treeControl, idx);
}
}
However, it somehow doesn’t recognize the triggers and so it does not fill my table.
I hoped to add a delete/insert trigger to the SecuryRoleTaskGrant systemtable, but this is completely closed, so I have no idea how or where this method can be triggered.
So then I thought as a ‘semi-working’ work around, let’s only log changes made in the front-end, since that already fixes the problem for the regular workers. So I decided to find a similar trigger within Forms, and I found the form SysSecUserAddRoles with method assignRolesToSelectedUser. This method has the following trigger:
if (added)
{
EePersonalDataAccessLogging::logUserRoleChange(secRole.RecId,0, userId, AddRemove::Add);
}
I decided to add this trigger to a Form that allows workers to change Task-Roles: SysSecRoleAddTasks method addTasksFromRole, and somehow this also does not seem to fix the problem. It doesn’t fill the table. I use the following code, and front-end changes are still not logged within my created EeRoleTaskChangeLog table.
public int addTasksFromRole(SecurityRole _role, boolean _recursive = false)
{
SecurityRole childRole;
SecuritySubRole subRole;
SecurityTask task;
SecurityRoleTaskGrant srtGrant;
int nAdded = 0;
RefRecId securityRole;
boolean added;
if (_role.RecId == targetRoleId)
{
return 0;
}
startLengthyOperation();
while select task
join SecurityRole, SecurityTask from srtGrant
where srtGrant.SecurityRole == _role.RecId
&& srtGrant.SecurityTask == task.RecId
{
nAdded += this.addTask(task);
}
if (_recursive)
{
while select childRole
where childRole.RecId != targetRoleId
join SecurityRole, SecuritySubRole from subRole
where subRole.SecuritySubRole == childRole.RecId
&& subRole.SecurityRole == _role.RecId
{
this.addTasksFromRole(childRole, true);
}
}
endLengthyOperation();
if (added) //Added
{
EePersonalDataAccessLogging::logRoleTaskChange(securityTask.RecId,0, securityRole, AddRemove::Add);
}
return nAdded;
}
Why doesn’t this trigger the logging?
Do I need to provide any more information?
The SysSecRoleAddTasks Form has also the addTask method, I tried it here as well, but this again doesn’t trigger the logtable to be filled.
public int addTask(SecurityTask _task, boolean _trialAdd = false)
{
SecurityRoleTaskGrant srtGrant;
int nAdded = 0;
boolean added; //Added
RefRecId securityRole; //Added
select targetRole
where targetRole.RecId == targetRoleId; // make sure in sync
select forupdate srtGrant
where srtGrant.SecurityTask == _task.RecId
&& srtGrant.SecurityRole == targetRoleId;
if (!srtGrant)
{
if (!_trialAdd)
{
ttsbegin;
srtGrant.SecurityTask = _task.RecId;
srtGrant.SecurityRole = targetRoleId;
srtGrant.insert();
nAdded++;
myObjects.insert(_task.RecId, true);
if (added) //Added
{
EePersonalDataAccessLogging::logRoleTaskChange(securityTask.RecId,0, securityRole, AddRemove::Add);
}
ttscommit;
}
else if (targetRoleId != 0)
{
nAdded++;
}
//info('Added task "' + _task.Name + '" (' + int2str(appl.ttsLevel()) + ')');
}
/*
else
{
info('Not adding task "' + _task.Name + '"');
} */
return nAdded;
}
Any suggestions?

This question requires a lot of hands-on debugging I would think to solve, which I'm not sure many posters are going to do unfortunately. Have you tried just using the system's database log?
SecurityRole and SecurityTask are system tables, stored under \System Documentation\Tables\SecurityRole, so I didn't know where to find them in the DB log wizard, but I wrote a manual job that I think should work.
I've only tested it with UserInfo, which is another system table and it appears to have logged changes correctly.
static void AddTablesToDBLog(Args _args)
{
void logTable(TableId _tableId, DatabaseLogType _logType)
{
DatabaseLog dblog;
dblog.logTable = _tableId;
dblog.logField = 0;
dblog.logType = _logType;
dblog.insert();
}
ttsBegin;
logTable(tableNum(SecurityRole), DatabaseLogType::Insert);
logTable(tableNum(SecurityRole), DatabaseLogType::Update);
logTable(tableNum(SecurityRole), DatabaseLogType::Delete);
logTable(tableNum(SecurityTask), DatabaseLogType::Insert);
logTable(tableNum(SecurityTask), DatabaseLogType::Update);
logTable(tableNum(SecurityTask), DatabaseLogType::Delete);
ttsCommit;
SysFlushDatabaseLogSetup::main();
new MenuFunction(menuitemDisplayStr(SysDatabaseLogSetup), MenuItemType::Display).run(null);
info("Done");
}

Related

How to use active() method x++

Ok I did it. It works fine. Thanks for help. Here is my code. Now I only need to call my command button in a differend form to disable it and create a info there. Anyone could look about it ? In my code I got reference errors.
[ExtensionOf(formdatasourcestr(ProdTableListPage, ProdTable))]
final class ProdParmReportFinishedActiveWG_Extension
{
public int active()
{
int ret;
next Active();
{
ProdTable tableBuffer = this.cursor();
ProdTable prodtable;
if(tableBuffer.ProdId == tableBuffer.CollectRefProdId
&& tableBuffer.ProdStatus != ProdStatus::ReportedFinished)
{
select firstonly RecId,ProdId from ProdTable where
ProdTable.CollectRefProdId == tableBuffer.ProdId
&& ProdTable.Prodstatus != ProdStatus::ReportedFinished
&& tableBuffer.RecId != prodtable.RecId;
{
Global::info(strFmt("%1 , %2",
prodtable.prodid, prodtable.recid));
// FormButtonControl mybutton = this.FormRun().design().controlname(formControlStr(ProdParmReportFinished, Ok)) as FormButtonControl;
// mybutton.enabled(false);
}
}
else
{
Global::info(strFmt("%1 , %2, %3, %4",
tableBuffer.prodid, tableBuffer.CollectRefProdId, tableBuffer.InventRefType, tableBuffer.ProdStatus));
}
}
return ret;
}
}
"I want to use this code everytime user changes his actual row but instead it runs just once and apply to all my rows."
Use the selectionChanged() method instead of active().
In fact most use cases where you think you should use active(), you're probably looking for selectionChanged() (or the OnSelectionChanged event for handlers) instead.

Add display method in a listpage form

I want to add a display method in the datasource of a listpage form. I can't add straightly because datasources come from an AOT query. How can I do it?
Thanks for help in advance.
The display method:
public display String255 UserNames()
{
DirPartyName partyName;
DirPersonUser personUser;
DirPerson person;
UserInfo userInfo;
String255 userList = "";
partyName = DirPersonUser::userId2Name(this.UserId);
if(partyName)
{
while select Name from person
exists join personUser
where personUser.PersonParty == person.RecId
&& personUser.User == this.UserId
{
userList += person.Name + ",";
}
}
if (!partyName)
{
while select Name from userInfo
where userInfo.Id == this.UserId
{
userList += userInfo.name + ",";
}
}
partyName = (select firstonly Name from userInfo where userInfo.Id == this.UserId).Name;
if (!partyName)
userList += this.UserId + ",";
strDel(userList,strLen(userList),1);
return userList;
}
I had a same requirement and we did it with a real physical field in the table TrvExpTable after the field is in the table we fill it on opening the list page.
In the init(), before the super(), of the form you can call that method:
public void updateTrvExpTableApprovers()
{
TrvExpTable trvExpTable;
TrvExpTrans trvExpTrans;
WorkflowWorkItemTable workflowItemTable;
String255 approversNames;
HcmWorker HcmWorker;
DirPersonUser dirPersonUser;
Name name;
ttsBegin;
while select forUpdate trvExpTable where trvExpTable.ApprovalStatus == TrvAppStatus::Pending
{
approversNames = "";
while select userId from workflowItemTable
where workflowItemTable.RefRecId == trvExpTable.RecId &&
workflowItemTable.RefTableId == trvExpTable.TableId &&
workflowItemTable.Status == WorkflowWorkItemStatus::Pending
{
select firstonly hcmWorker
join firstonly PersonParty, ValidFrom, ValidTo from dirPersonUser
where dirPersonUser.User == workflowItemTable.userId &&
hcmWorker.Person == dirPersonUser.PersonParty;
name = HcmWorker.name();
if (!name)
{
name = workflowItemTable.userId;
}
if(approversNames)
{
if (!strScan(approversNames, name, 0, 255))
{
approversNames += ', ' + name;
}
}
else
{
approversNames = name;
}
}
trvExpTable.ApproversNames = approversNames;
trvExpTable.update();
}
update_recordSet trvExpTable
setting ApproversNames = ""
where trvExpTable.ApprovalStatus != TrvAppStatus::Pending && trvExpTable.ApproversNames != "";
ttsCommit;
}
It is not the most efficient way but in our case it is working perfect for more than a year now.
Also that way the user could CTRL+G on that field in the grid, which is not a option using the display methods.
Best Regards,
Kristian
Why don't you try writing display method in table method as use same in list page? That is good option.
If you still want to write display method in form data-source level then change your display method like below.
public display String255 UserNames(DataSourcetablename _tableName)
{ ....
Use _tableName where ever you are using "this".
You can refer PurchEditLines form in standard Ax 2012 and see
display ImageRes backOrder(PurchParmLine _purchParmLine).
For more details on display method click here

ASP.NET Entity Framework DataContext use issue

I 've built an ASP.NET website using EF. I created a DataContext class which implements the singleton pattern. My DAO classes (singletons too) instanciate this datacontext and store it in a property. They use it in order to query the SQLServer DataBase. This worked ok for 3 months but I suddenly got exception messages like :"Connection must be valid and open / connection already open". It seemed that datacontext was not disposed. The only change, according to me, was the data size and number of users increasing.
I then found multiple posts saying that singleton was a bad idea foe datacontext, so I tried to instanciate datacontext in a using statement in every request and that resolved the problem, except for update queries which had no effects in database. I had to attach the db object to the context and then set its EntityState to "modified" to have my SaveChanges work.
Like this :
public bool DoucheXpsu(as_headers session) {
using (MyDBEntities MyContext = new MyDBEntities()) {
try {
as_status status = GetStatus(session);
if (status != null) {
if (status.mainstatusvalue == 300) {
status.DateDoucheXpsu = DateTime.Now;
status.DoucheXpsu = 1;
MyContext.as_status.Attach(status);
MyContext.ObjectStateManager.ChangeObjectState(status, EntityState.Modified);
MyContext.SaveChanges();
return true;
} else {
return false;
}
} else {
return false;
}
} catch (OptimisticConcurrencyException) {
return false;
} catch (Exception) {
return false;
}
}
}
The problem is that it actually didn't work for ONE method (which has nothing different from the other update method) !
The exception occured as I tried to attach the object : "The object cannot be attached because it is already in the object context. An object can only be reattached when it is in an unchanged state. " So I had to comment the attach and ChangeObjectState methods to have it work as expected :
public bool SetSessionToDelete(string numSession) {
using (MyDBEntities MyContext = new MyDBEntities()) {
try {
view_headerStatus view = (from v in MyContext.view_headerStatus
where v.CodeSession == numSession
where v.lastinserted == 1
select v).First();
if (view != null) {
as_status status = (from s in MyContext.as_status
where s.jobclsid == view.jobclsid
where s.lastinserted == 1
select s).First();
if (status != null) {
status.DeleteSession = 1;
//MyContext.as_status.Attach(status);
//MyContext.ObjectStateManager.ChangeObjectState(status, EntityState.Modified);
MyContext.SaveChanges();
return true;
} else {
return false;
}
} else {
return false;
}
} catch (OptimisticConcurrencyException) {
return false;
} catch (Exception) {
return false;
}
}
}
The question is WHY should this one behave differently ???
I've read many posts about EF and dataContext but I feel I'm missing something. I would be glad if anyone can help.
Thanks.
In your first example, this line here:
as_status status = GetStatus(session);
I would assume this populates using a DIFFERENT context, and when it leaves the GetStatus() method the context it used to load is disposed. That is why your subsequent Attach() works. However in your second example you do not need to attach because it was loaded using the current (connected) context.
To solve you may want to either pass the context to your methods like GetStatus() resulting in no need to reattach. I don't typically reattach unless I am resurrecting an object over the wire or from a file.

How to persist search criteria and results of a c# .NET application page

Scenario:
User submits search criteria and selects an item from search results on the same page, which navigates to a new page of details for the selected item.
When the User returns to the search screen, the search criteria & results (including selected page and sort-order) should be preserved from their last visit.
Related information:
All form submissions are POSTs.
Navigation back to the search screen may not be available from last browser history (e.g. more than one details screen may be encountered, or the user may navigate directly to the search screen from an alternative menu.)
Search results are provided using Telerik RadGrid control.
I'm looking for a generic solution that will be able to be applied to different search screens.
In some instances, the item may be DELETED from within the details screen, and should therefore not appear in the search results when the screen is next encountered.
Thoughts:
I've read a lot of suggested methods for addressing various parts of this scenario, but I'm still confused; no comprehensively "correct" solution jumps to the forefront.
I guess I'm asking for recommendations/approach rather than a whole solution spelled out for me (although that would be nice! ;-)
The .NET VIEWSTATE would seem to do exactly what I'm after (with the exception of #5) - Is there some way of leveraging off this so that viewstate can be used between pages, and not just between postbacks to the same page? (e.g. can I store/restore viewstate to/from a session variable or something? I haven't seen this suggested anywhere and I'm wondering if there's a reason why.)
Thanks in advance.
Thanks for all the advice.
For the benefit of others, here is a solution to this issue (no doubt there's room for improvement, but this works satisfactorily for the moment).
4 functions...
StoreSearchCookie - Persist the state/values of a panel of search criteria and a results grid in a cookie with a specified UID.
RestoreSearchCookie_Criteria - Read the cookie and re-populate the search criteira
RestoreSearchCookie_Results - Read the cookie and restore the grid state.
FindFormControls - Helper method to recursively find form-input controls in a container (pinched & modified from elsewhere on Stack Overflow)
NB...
I haven't addressed the multiple-tabs issue because our application doesn't allow them anyway.
RestoreSearchResults utilises GridSettingsPersister.cs available from Telerik website, (but I had to modify it to store the page number as well)
Usage is as follows...
protected void Page_PreRender(object sender, EventArgs e)
{
if (Page.IsPostBack)
{
// Store the state of the page
StoreSearchCookie("SomeSearchPage", pnlSearchCriteria, gridResults);
}
else
{
// Restore search criteria
RestoreSearchCookie_Criteria("SomeSearchPage");
// Re-invoke the search here
DoSearch(); // (for example)
// Restore the grid state
RestoreSearchCookie_Results("SomeSearchPage");
}
}
Code follows...
protected void StoreSearchCookie(string cookieName, Panel SearchPanel, RadGrid ResultsGrid)
{
try
{
HttpCookie cookieCriteria = new HttpCookie("StoredSearchCriteria_" + cookieName);
// 1. Store the search criteria
//
List<Control> controls = new List<Control>();
FindFormControls(controls, SearchPanel);
foreach (Control control in controls)
{
string id = control.ID;
string parentId = control.Parent.ID;
string uid = string.Format("{0}>{1}", parentId, id);
string value = "";
Type type = control.GetType();
bool isValidType = true; // Optimistic!
if (type == typeof(TextBox))
{
value = ((TextBox)control).Text;
}
else if (type == typeof(DropDownList))
{
value = ((DropDownList)control).SelectedValue;
}
else if (type == typeof(HiddenField))
{
value = ((HiddenField)control).Value;
}
else if (type == typeof(RadioButton))
{
value = ((RadioButton)control).Checked.ToString();
}
else if (type == typeof(CheckBox))
{
value = ((CheckBox)control).Checked.ToString();
}
else
{
isValidType = false;
}
if (isValidType)
{
cookieCriteria.Values[uid] = value;
}
}
cookieCriteria.Expires = DateTime.Now.AddDays(1d);
Response.Cookies.Add(cookieCriteria);
// 2. Persist the grid settings
//
GridSettingsPersister SavePersister = new GridSettingsPersister(ResultsGrid);
HttpCookie cookieResults = new HttpCookie("StoredSearchResults_" + cookieName);
cookieResults.Values["GridId"] = ResultsGrid.ID;
cookieResults.Values["GridSettings"] = SavePersister.SaveSettings();
cookieResults.Expires = DateTime.Now.AddDays(1d);
Response.Cookies.Add(cookieResults);
}
catch (Exception exception)
{
Logger.Write(exception);
}
}
protected void RestoreSearchCookie_Criteria(string cookieName)
{
try
{
HttpCookie cookieCriteria = Request.Cookies["StoredSearchCriteria_" + cookieName];
if (cookieCriteria != null)
{
foreach (string key in cookieCriteria.Values.AllKeys)
{
string value = cookieCriteria[key];
string[] ids = key.Split('>');
string parentId = ids[0];
string id = ids[1];
Control control = FindControl(parentId).FindControl(id);
Type type = control.GetType();
if (type == typeof(TextBox))
{
((TextBox)control).Text = value;
}
else if (type == typeof(DropDownList))
{
((DropDownList)control).SelectByValue(value);
}
else if (type == typeof(HiddenField))
{
((HiddenField)control).Value = value;
}
else if (type == typeof(RadioButton))
{
((RadioButton)control).Checked = Boolean.Parse(value);
}
else if (type == typeof(CheckBox))
{
((CheckBox)control).Checked = Boolean.Parse(value);
}
}
}
}
catch (Exception exception)
{
Logger.Write(exception);
}
}
protected void RestoreSearchCookie_Results(string cookieName)
{
try
{
HttpCookie cookieResults = Request.Cookies["StoredSearchResults_" + cookieName];
if (cookieResults != null)
{
string gridId = cookieResults.Values["GridId"];
string settings = cookieResults.Values["GridSettings"];
RadGrid grid = (RadGrid)FindControl(gridId);
GridSettingsPersister LoadPersister = new GridSettingsPersister(grid);
LoadPersister.LoadSettings(settings);
grid.Rebind();
}
}
catch (Exception exception)
{
Logger.Write(exception);
}
}
private void FindFormControls(List<Control> foundSofar, Control parent)
{
List<Type> types = new List<Type> { typeof(TextBox), typeof(DropDownList), typeof(RadioButton), typeof(CheckBox), typeof(HiddenField) };
foreach (Control control in parent.Controls)
{
if (types.Any(item => item == control.GetType()))
{
foundSofar.Add(control);
}
if (control.Controls.Count > 0)
{
this.FindFormControls(foundSofar, control); // Use recursion to find all descendants.
}
}
}

X++ passing current selected records in a form for your report

I am trying to make this question sound as clear as possible.
Basically, I have created a report, and it now exists as a menuitem button so that the report can run off the form.
What I would like to do, is be able to multi-select records, then when I click on my button to run my report, the current selected records are passed into the dialog form (filter screen) that appears.
I have tried to do this using the same methods as with the SaleLinesEdit form, but had no success.
If anyone could point me in the right direction I would greatly appreciate it.
Take a look at Axaptapedia passing values between forms. This should help you. You will probably have to modify your report to use a form for the dialog rather than using the base dialog methods of the report Here is a good place to start with that!
Just wanted to add this
You can use the MuliSelectionHelper class to do this very simply:
MultiSelectionHelper selection = MultiSelectionHelper::createFromCaller(_args.caller());
MyTable myTable = selection.getFirst();
while (myTable)
{
//do something
myTable = selection.getNext();
}
Here is the resolution I used for this issue;
Two methods on the report so that when fields are multi-selected on forms, the values are passed to the filter dialog;
private void setQueryRange(Common _common)
{
FormDataSource fds;
LogisticsControlTable logisticsTable;
QueryBuildDataSource qbdsLogisticsTable;
QueryBuildRange qbrLogisticsId;
str rangeLogId;
set logIdSet = new Set(Types::String);
str addRange(str _range, str _value, QueryBuildDataSource _qbds, int _fieldNum, Set _set = null)
{
str ret = _range;
QueryBuildRange qbr;
;
if(_set && _set.in(_Value))
{
return ret;
}
if(strLen(ret) + strLen(_value) + 1 > 255)
{
qbr = _qbds.addRange(_fieldNum);
qbr.value(ret);
ret = '';
}
if(ret)
{
ret += ',';
}
if(_set)
{
_set.add(_value);
}
ret += _value;
return ret;
}
;
switch(_common.TableId)
{
case tableNum(LogisticsControlTable):
qbdsLogisticsTable = element.query().dataSourceTable(tableNum(LogisticsControlTable));
qbrLogisticsId = qbdsLogisticsTable.addRange(fieldNum(LogisticsControlTable, LogisticsId));
fds = _common.dataSource();
for(logisticsTable = fds.getFirst(true) ? fds.getFirst(true) : _common;
logisticsTable;
logisticsTable = fds.getNext())
{
rangeLogId = addrange(rangeLogId, logisticsTable.LogisticsId, qbdsLogisticsTable, fieldNum(LogisticsControlTable, LogisticsId),logIdSet);
}
qbrLogisticsId.value(rangeLogId);
break;
}
}
// This set the query and gets the values passing them to the range i.e. "SO0001, SO0002, SO000£...
The second methods is as follows;
private void setQueryEnableDS()
{
Query queryLocal = element.query();
;
}
Also on the init method this is required;
public void init()
{
;
super();
if(element.args() && element.args().dataset())
{
this.setQueryRange(element.args().record());
}
}
Hope this helps in the future for anyone else who has the issue I had.

Resources