Enums are/will be the Option replacements for Business Central 365. Recently I had an occasion to use a few and get my feet wet, so to speak. As seems to be the case far too often, about 80% of the functionality you need is readily available, but the remaining 20% takes more work than it should.
In the case of Enums, you get a Text list of Names and an Integer list of associated Ordinal values, but you do NOT get a list of Captions. In the partial example Enum FourStates below, Default, OH and TX are the Names, 0, 1 and 2 are the Ordinals and space, Ohio and Texas are the captions. Note that Ordinals are the defined numeric values, NOT the indexes. Perfectly valid Ordinals in the example below could be 1, 5 and 7.
value(0; Default) { Caption = ' '; }
value(1; OH) { Caption = 'Ohio'; }
value(2; TX) { Caption = 'Texas'; }
If you define a Table or Page field as an Enum, then the captions are displayed in the dropdowns. To get the Caption you can use Format(EnumType::Name) but we needed to iterate all of the Captions for a given Enum.
After digging around in some blogs and the documentation, here is a summary of what I found.
First, there is a major limitation because Captions can ONLY be processed within the context of an Enum type and, at least as of now, the Business Central 365 SaaS solution has no support for generic Enums (e.g. EnumRef like RecordRef and FieldRef). This means a Text list of Captions must be created on an Enum by Enum basis.
However, once the Captions list has been created, then using the combination of Name, Ordinal and Caption Lists of [Text], you can then write general purpose code that will work for any Enum.
A Gotcha, when you use the TEnum code snippet in VS Code, it defaults the first element to 0. We have learned to either make that the Default as the partial code above does, or change it to 1. The reason for this is because if there is any chance that you will ever use an Enum with the StrMenu command, StrMenu defaults 0 as the Cancel return value. In that case, you could never select the 0 Ordinal Enum because ti would be treated as Canceled.
To create an instance of an Enum to use within your code, create a Var variable similar to MyChoices: Enum "Defined Choices";. If you also define a MyChoice: Enum: "Defined Choices", you can compare them with something like if MyChoice = MyChoices::FirstChoice then, etc.
Below is some sample code with a few different Enum styles along with a method that allows you to create a List of [Text] for the Captions. Again, that has to be coded for each specific Enum.
a. Within VS Code, use AL Go to create a new HelloWorld app
b. Change the app.json file Name and Publisher, I named mine EnumHelper
NOTE: We always define a Publisher because you cannot filter for Default Publisher within SaaS
c. Replace all of the code inside the HelloWorld.al file with the code below
NOTE: To simplify things, everything below is all in the same file
d. The code is a PageExtension for the Chart of Accounts Page and runs off the OnOpenPage trigger
This allows the code to be easily called without requiring a bunch of setup code.
Here is the code that allows you to create a Captions list. The variable myOrdinal is an Integer and Flagged is a defined Enum. Note the Enum::EnumName, similar to Page::PageName or Database::TableName.
foreach myOrdinal in Flagged.Ordinals do begin
// Enum definition, NOT an enum instance.
captions.Add(Format(Enum::Flagged.FromInteger(myOrdinal)));
end;
All of the code (sorry, it didn't format exactly correctly)
enum 50200 FourStates
{
Extensible = true;
value(0; Default) { Caption = ' '; }
value(1; OH) { Caption = 'Ohio'; }
value(2; TX) { Caption = 'Texas'; }
value(3; NC) { Caption = 'North Carolina'; }
value(4; IA) { Caption = 'Iowa'; }
value(5; MO) { Caption = 'Missouri'; }
}
enum 50201 Flagged
{
Extensible = true;
value(0; Default) { Caption = ' '; }
value(1; Bold) { Caption = 'Bold'; }
value(2; ITalic) { Caption = 'Italid '; }
value(4; Underline) { Caption = 'Underline'; }
value(8; BoldItalic) { Caption = 'Bold & Italic'; }
value(16; BoldUnderline) { Caption = 'Bold & Underline '; }
value(32; ItalicUnderline) { Caption = 'Italic & Underline'; }
value(64; All3Options) { Caption = 'All 3 Options'; }
}
enum 50202 Randomized
{
Extensible = true;
value(0; Default) { Caption = ' '; }
value(7; Good) { Caption = 'The Good'; }
value(5; Bad) { Caption = 'The Bad'; }
value(11; Ugly) { Caption = 'The Ugly'; }
}
enum 50203 ProcessFlowOptions
{
Extensible = true;
value(0; Default) { Caption = ' '; }
value(1; Flagged) { Caption = 'Flagged'; }
value(2; Randomized) { Caption = 'Randomized'; }
value(4; FourStates) { Caption = 'FourStates'; }
}
pageextension 50200 "Chart of Accounts EH" extends "Chart of Accounts"
{
var
// Enum instance variables.
myFlagged: Enum Flagged;
myFourStates: Enum FourStates;
myRandomized: Enum Randomized;
trigger OnOpenPage();
begin
case UserID.ToLower() of
'larry':
Message('Hello Larry, this is an extension for October testing.');
'vicki':
Message('Good morning Vicki, this is an extension for October testing.');
else
if Confirm('Hello %1 from EnumHelper.\\Click Yes to process or no to cancel.', true, UserID) then begin
ProcessEnumerations();
end;
end;
end;
local procedure ProcessEnumerations()
var
allLines: TextBuilder;
randomCaptions: List of [Text];
flaggedCaptions: List of [Text];
fourStatesCaptions: List of [Text];
begin
GetEnumCaptions(randomCaptions, flaggedCaptions, fourStatesCaptions);
IterateEnumNamesOrdinalsAndCaptions(allLines, randomCaptions, flaggedCaptions, fourStatesCaptions);
Message(allLines.ToText());
end;
local procedure GetEnumCaptions(randomCaptions: List of [Text]; flaggedCaptions: List of [Text]; fourStatesCaptions: List of [Text])
begin
GetCaptions(randomCaptions, ProcessFlowOptions::Randomized);
GetCaptions(flaggedCaptions, ProcessFlowOptions::Flagged);
GetCaptions(fourStatesCaptions, ProcessFlowOptions::FourStates);
end;
local procedure IterateEnumNamesOrdinalsAndCaptions(allLines: TextBuilder; randomCaptions: List of [Text]; flaggedCaptions: List of [Text]; fourStatesCaptions: List of [Text])
begin
IterateEnumNamesOrdinalsAndCaptions('Flagged Enum', allLines, myFlagged.Names, myFlagged.Ordinals, flaggedCaptions);
IterateEnumNamesOrdinalsAndCaptions('Randomized Enum', allLines, myRandomized.Names, myRandomized.Ordinals, randomCaptions);
IterateEnumNamesOrdinalsAndCaptions('FourStates Enum', allLines, myFourStates.Names, myFourStates.Ordinals, fourStatesCaptions);
end;
local procedure IterateEnumNamesOrdinalsAndCaptions(title: Text; allLines: TextBuilder; enumNames: List of [Text]; enumOrdinals: List of [Integer]; enumCaptions: List of [Text])
var
i: Integer;
enumLine: TextBuilder;
enumLines: TextBuilder;
begin
allLines.AppendLine(title);
allLines.appendLine();
for i := 1 to enumNames.Count do begin
Clear(enumLine);
enumLine.AppendLine('EnumName: ''' + enumNames.Get(i) + ''',');
enumLine.AppendLine('EnumOrdinal: ' + Format(enumOrdinals.Get(i)) + ',');
enumLine.AppendLine('EnumCaption: ''' + enumCaptions.Get(i) + '''.');
//enumLine.AppendLine('EnumName: ''' + enumNames.Get(i) + ''', EnumOrdinal: ' + ordinal + ', EnumCaption: ''' + enumCaptions.Get(i) + '''');
enumLines.AppendLine(enumLine.ToText());
end;
allLines.AppendLine(enumLines.ToText());
end;
local procedure GetCaptions(captions: List of [Text]; processFlowOption: Enum ProcessFlowOptions)
var
myOrdinal: Integer;
myProcessFlowOptions: Enum ProcessFlowOptions;
begin
// Load captions by iterating specific Enums.
case processFlowOption of
myProcessFlowOptions::Flagged:
begin
foreach myOrdinal in Flagged.Ordinals do begin
// Enum definition, NOT an enum instance.
captions.Add(Format(Enum::Flagged.FromInteger(myOrdinal)));
end;
end;
myProcessFlowOptions::Randomized:
begin
foreach myOrdinal in Randomized.Ordinals do begin
// Enum definition, NOT an enum instance.
captions.Add(Format(Enum::Randomized.FromInteger(myOrdinal)));
end;
end;
myProcessFlowOptions::FourStates:
begin
foreach myOrdinal in FourStates.Ordinals do begin
// Enum definition, NOT an enum instance.
captions.Add(Format(Enum::FourStates.FromInteger(myOrdinal)));
end;
end;
end;
end;
}
Enjoy
I have implemented a better workaround for BC14. That should work on newer versions also, but I have tested on BC14 only.
var
RecRef: RecordRef;
FRef: FieldRef;
OptionNames: List of [Text];
OptionCaptions: List of [Text];
i: Integer;
RecRef.GetTable(Rec); // Some record
FRef := RecRef.Field(20); // Some field of type Enum
OptionNames := FRef.OptionMembers().Split(',');
OptionCaptions := FRef.OptionCaption().Split(',');
for i := 1 to OptionNames.Count() do begin
Evaluate(FRef, OptionNames.Get(i));
Message('Ordinal = %1\Name = %2\Caption = %3',
format(FRef, 0, 9),
OptionNames.Get(i),
format(FRef, 0, 1)); // or OptionCaptions.Get(i)
end;
Related
I have a stored procedure with the following signature and local variables:
PROCEDURE contract_boq_import(i_project_id IN RAW,
i_boq_id IN RAW,
i_master_list_version IN NUMBER,
i_force_update_if_exists IN BOOLEAN,
i_user_id IN NUMBER,
o_boq_rev_id OUT RAW) AS
v_contract_id RAW(16);
v_contract_no VARCHAR2(100);
v_series_rev_id_count NUMBER(1);
v_project_id_count NUMBER(5);
v_now DATE;
v_boq_import_rev_id RAW(16);
v_master_project_id RAW(16);
v_prj_duplicate_items VARCHAR2(1000) := '';
I set up an output parameter using one of our DAL utilities:
var revParam = new byte[16];
dataHandler.CreateParameterRaw("o_boq_rev_id", revParam).Direction = ParameterDirection.Output;
Where CreateParameterRaw is declared as:
public DbParameter CreateParameterRaw(string name, object value)
{
OracleParameter oracleParameter = new OracleParameter();
oracleParameter.ParameterName = name;
oracleParameter.OracleDbType = OracleDbType.Raw;
oracleParameter.Value = value;
this.Parameters.Add((DbParameter) oracleParameter);
return (DbParameter) oracleParameter;
}
Then when I execute the procedure with ExecuteNonQuery I get the following error:
Oracle.ManagedDataAccess.Client.OracleException
HResult=0x80004005
Message=ORA-06502: PL/SQL: numeric or value error: raw variable length too long
ORA-06512: at "ITIS_PRCDRS.PA_PRJ_IMP", line 1235
The exception is thrown on line 1235:
o_boq_rev_id := v_boq_import_rev_id;
As you can see from the procedure declaration above, v_boq_import_rev_id has type RAW(16) and o_boq_rev_id has type OUT RAW, so why should the assignment on line 1235 fail? What am I doing wrong?
PS: The proc executes fine when I call it in plain PL/SQL.
In OracleParameter the default size is 0 for the parameters that may have size values. (Official reference here.)
That is why you need to modify your method which generates the raw values. Below you can find the modified method:
public DbParameter CreateParameterRaw(string name, object value, int parameterSize)
{
OracleParameter oracleParameter = new OracleParameter();
oracleParameter.ParameterName = name;
oracleParameter.OracleDbType = OracleDbType.Raw;
oracleParameter.Value = value;
oracleParameter.Size = parameterSize; /* THIS IS THE ADDED PARAMETER */
this.Parameters.Add((DbParameter) oracleParameter);
return (DbParameter) oracleParameter;
}
And as a result you can pass the size while calling CreateParameterRaw as you did in your existing code:
var revParam = new byte[16];
/* CHECK THE 16 value in the parameters that are sent to CreateParameterRaw */
dataHandler.CreateParameterRaw("o_boq_rev_id", revParam, 16).Direction = ParameterDirection.Output;
Additional suggestion: In order to keep apples with apples, I would suggest you can take Direction parameter also into the CreateParameterRawmethod. By this way CreateParameterRawbecomes the whole responsible about generating the parameters.
Credits:
Official page: https://docs.oracle.com/en/database/oracle/oracle-database/12.2/odpnt/ParameterCtor5.html#GUID-04BE7E69-A80A-4D28-979A-CDC2516C0F93
A blog that has similar problem: http://devsilos.blogspot.com/2013/01/ora-06502-with-out-parameter-called.html?m=1
Size usage from Microsoft: https://learn.microsoft.com/en-us/dotnet/api/system.data.oracleclient.oracleparameter.size?view=netframework-4.8
This is an interesting problem with a weird solution.
Actually while using the RAW in output parameter you MUST provide some buffer space for it when adding this parameter.
Can you please provide some buffer space for this variable and try something like the following:
byte[] RAWPlaceholder = new byte[16];
cmd.AddParameter(new OracleParameter("o_boq_rev_id",
OracleDbType.Raw,
16,
RAWPlaceholder,
ParameterDirection.Output);
Please share the result of the aforementioned exercise.
Thanks
I have to check a value of TEdit->Text when a user leaves it and return him to the TEdit, if the value is wrong. The code below works nice in a VCL but it doesn't work in a FMX. So it beeps but doesn't return.
void __fastcall TForm1::Edit1Exit(TObject *Sender)
{
if (Edit1->Text != "123")
{
Beep();
Edit1->SetFocus();
}
}
It is in a simple form with 2 TEdits only. What I do wrong and how to do it right?
I'll provide a solution in Delphi Firemonkey. Hopefully, the same principles apply in C++ Firemonkey. The following code replaces the invalid text in Edit1 with the word 'Invalid' and returns the focus to Edit1, with 'Invalid' selected ready for overtyping.
procedure TForm1.Edit1Validate(Sender: TObject; var Text: string);
begin
if Text <> '123' then
begin
Text := 'Invalid';
TThread.CreateAnonymousThread(
procedure
begin
TThread.Synchronize(nil,
procedure
begin
Edit1.SetFocus;
end);
end).Start;
end;
end;
I wrote a simple program:
void __fastcall TForm1::Edit1Exit(TObject *Sender)
{
TThread::CreateAnonymousThread(
[=]()
{
TThread::Synchronize (NULL, [=](){
Memo1->Lines->Add("Edit1->SetFocus()");
});
})->Start();
Memo1->Lines->Add("Edit1 exit");
}
//--------------------------------------------------------------------------
void __fastcall TForm1::Edit2Enter(TObject *Sender)
{
Memo1->Lines->Add("Edit2 enter");
}
There are three controls on a form: 2 TEdits and a TMemo1. When I run it and jump from the TEdit1 to the TEdit2, I have on the memo:
Edit1 exit
Edit2 enter
Edit1->SetFocus()
Without threads, I would have
Edit1->SetFocus()
Edit1 exit
Edit2 enter
With threads, but without Synchronize I have
Edit1 exit
Edit1->SetFocus()
Edit2 enter
So, what Synchronize() synchronizes with what stays unclear for me.
In my shell extension, I want to mimic explorer's behavior and show 'This folder is empty' message when in fact my folder is empty:
However, I can't accomplish it.
Using API Monitor, I see that when explorer refreshes an empty folder, IEnumIDList::Next() is returning the following:
Meaning, that the 'next' item returned is NULL, the number of items is 0 and the result is S_FALSE.
As mentioned, I tried to mimic the return values, and indeed no items are loaded for the folder, but no message appear either.
So what API would trigger this message?
Your IEnumIDList implementation must implement IObjectWithSite. Sample of implementation:
var
ServiceProvider: IServiceProvider;
ShellBrowser: IShellBrowser;
ShellView: IShellView;
FolderView2: IFolderView2;
begin
if not Assigned(ASite) then Exit;
OleCheck(ASite.QueryInterface(IServiceProvider, ServiceProvider));
try
OleCheck(ServiceProvider.QueryService(SID_STopLevelBrowser, IShellBrowser, ShellBrowser));
try
OleCheck(ShellBrowser.QueryActiveShellView(ShellView));
try
OleCheck(ShellView.QueryInterface(IFolderView2, FolderView2));
try
FolderView2.SetText(FVST_EMPTYTEXT, 'The message you want to see');
finally
FolderView2 := nil;
end;
finally
ShellView := nil;
end;
finally
ShellBrowser := nil;
end;
finally
ServiceProvider := nil;
end;
end;
Result:
Also you can use the same code in your IShellFolder implementation.
This is an old question but I write my answer. It might be useful to someone
You can change the empty folder view text in IShellFolder::CreateViewObject method, like below:
if (riid == IID_IShellView)
{
m_hwndOwner = hwndOwner;
SFV_CREATE sfvData = { sizeof sfvData };
const CComQIPtr<IShellFolder> spFolder = GetUnknown();
sfvData.psfvcb = this;
sfvData.pshf = spFolder;
auto hr = SHCreateShellFolderView(&sfvData, reinterpret_cast<IShellView**>(ppv));
// Here we change the text
auto pSV = *reinterpret_cast<IShellView**>(ppv);
CComPtr<IFolderView2> pFV;
pSV->QueryInterface(IID_PPV_ARGS(&pFV));
pFV->SetText(FVST_EMPTYTEXT, L"Put your text here!");
//-
return hr;
}
I have this code:
public int GetUserIdByEmail(string email)
{
using (SqlConnection conn = new SqlConnection(ZincModelContainer.CONNECTIONSTRING))
{
using (SqlCommand cmd = conn.CreateCommand())
{
conn.Open();
cmd.CommandType = System.Data.CommandType.Text;
cmd.CommandText = String.Concat("SELECT [Zinc].[GetUserIdByEmail] (", email, ")"); //is this correct??? the problem lies here
return (int)cmd.ExecuteScalar();
}
}
}
I get the error here in above code. this is still not right
I have my function now as below suggested by veljasije
thanks
Modify your procedure:
CREATE PROCEDURE [Zinc].[GetUserIdByEmail]
(
#Email varchar (100)
)
AS
BEGIN
SELECT zu.UserId from Zinc.Users zu WHERE Email = #Email
END
And in you code change type of parameter from NVarChar to VarChar
Function
CREATE FUNCTION [Zinc].[GetUserIdByEmail]
(
#Email varchar(100)
)
RETURNS int
AS
BEGIN
DECLARE #UserId int;
SET #UserId = (SELECT zu.UserId from Zinc.Users zu WHERE Email = #Email)
RETURN #UserId
END
Firstly, specify the size for the #Email parameter in the sproc - without it, it will default to 1 character which will therefore not be attempting to match on the value you are expecting it to.
Always specify the size explicitly to avoid any issues (e.g. per Marc_s's comment, plus demo I blogged about here, it behaves differently bu defaulting to 30 chars when using CAST/CONVERT )
Secondly, use SqlCommand.ExecuteScalar()
e.g.
userId = (int)cmd.ExecuteScalar();
Suppose I have a GDI+ GraphicsPath that is relatively complex, with "holes" in it. Text is a good example, like the letter "O". I want to transform this path so that I can fill it in completely, including the "holes." How can I do this?
Cody,
I didn't see that you had accepted an answer yet, so I am putting this C# function here for you to see if it helps. It has been tested.
Slightly different from the version above: This routine looks for the path with the largest bounding area, so it is a bit more generous than the version above since it doesn't require the "master" path to pass 2 tests to prove that it is worthy of keeping.
I made this into an extension method, so in .Net you can just write:
GraphicsPath solid = LetterPath.ToSolidPath();
The return value is a new GraphicsPath whose interior has been eviscerated (wow, I don't get to use that word very often).
/// <summary>
/// Removes all subpaths (holes) from a graphics path, leaving only the largest whole path behind
/// </summary>
public static GraphicsPath ToSolidPath(this GraphicsPath path)
{
GraphicsPath BiggestPath = null;
GraphicsPath SubPath = new GraphicsPath();
RectangleF BoundsRect = RectangleF.Empty;
RectangleF BiggestRect = RectangleF.Empty;
bool bIsClosed = false;
var pathIterator = new GraphicsPathIterator(path);
pathIterator.Rewind();
for (int i = 0; i < pathIterator.SubpathCount; i++)
{
SubPath.Reset();
pathIterator.NextSubpath(SubPath, out bIsClosed);
BoundsRect = SubPath.GetBounds();
if (BoundsRect.Width * BoundsRect.Height > BiggestRect.Width * BiggestRect.Height)
{
BiggestRect = BoundsRect;
BiggestPath = (GraphicsPath)SubPath.Clone();
}
}
return BiggestPath;
}
Here's a partial solution, in Delphi, that I came up with. It only works in cases where the entire path is "contained" within a single subpath. It simply iterates the subpaths and returns a new path identical to the biggest subpath. It is not a universal solution to the problem above, but it works for the case at hand, and might help someone else in the future:
function BlockPath(Path: IGPGraphicsPath): IGPGraphicsPath;
var
PathIterator: IGPGraphicsPathIterator;
SubPath: IGPGraphicsPath;
I: Integer;
IsClosed: Boolean;
BiggestPath: IGPGraphicsPath;
BiggestRect, BoundsRect: TGPRectF;
begin
Result := TGPGraphicsPath.Create;
SubPath := TGPGraphicsPath.Create;
PathIterator := TGPGraphicsPathIterator.Create(Path);
PathIterator.Rewind;
BiggestPath := nil;
BiggestRect.Width := 0;
BiggestRect.Height := 0;
for I := 0 to PathIterator.SubpathCount - 1 do
begin
SubPath.Reset;
PathIterator.NextSubPath(SubPath, IsClosed);
SubPath.GetBounds(BoundsRect);
if (BoundsRect.Width >= BiggestRect.Width) and
(BoundsRect.Height >= BiggestRect.Height) then
begin
BiggestRect := BoundsRect;
BiggestPath := SubPath.Clone;
end;
end;
if BiggestPath <> nil then
begin
Result.AddPath(BiggestPath, True);
end;
end;
Comments and improvements are welcome.