How to dynamically add a content part in a Orchard content handler? - orchardcms

I have an Orchard content handler that calls
Filters.Add(new Orchard.ContentManagement.Handlers.ActivatingFilter<MyPart>("User"));
in its constructor to weld MyPart to a user content item.
How can i weld MyPart based on the content item id?
The issue here is that the content item is not yet created when the constructor is called. I tried hooking into the life cycle with overriding Activating() but that doesn't work either as the content item is also not created yet.

Okay, this task is really difficult. Here's my solution.
1) Create an extension method that welds a content part to a content item (sadly, we cannot use ContentItemBuild.Weld() as there's no chance to pass the content item)
// adopted from ContentItemBuilder.Weld<>()
public static TPart Weld<TPart>(this Orchard.ContentManagement.ContentItem aContentItem)
where TPart: Orchard.ContentManagement.ContentPart, new()
{
var partName = typeof(TPart).Name;
// obtain the type definition for the part
var typePartDefinition = aContentItem.TypeDefinition.Parts.FirstOrDefault(p => p.PartDefinition.Name == partName);
if (typePartDefinition == null) {
// If the content item's type definition does not define the part; use an empty type definition.
typePartDefinition = new Orchard.ContentManagement.MetaData.Models.ContentTypePartDefinition(
new Orchard.ContentManagement.MetaData.Models.ContentPartDefinition(partName),
new Orchard.ContentManagement.MetaData.Models.SettingsDictionary());
}
// build and weld the part
var part = new TPart { TypePartDefinition = typePartDefinition };
aContentItem.Weld(part);
return part;
}
2) Define a StorageFilter for dynamically welding the content part to the content item
public class BaseWeldBeforeStorageFilter<TPart, TRecord> : Orchard.ContentManagement.Handlers.IContentStorageFilter
where TPart: Orchard.ContentManagement.ContentPart, new()
where TRecord: Orchard.ContentManagement.Records.ContentPartRecord
{
// public
public BaseWeldBeforeStorageFilter(Orchard.Data.IRepository<TRecord> aPartRecords)
{
mPartRecords = aPartRecords;
}
...
public void Loading(Orchard.ContentManagement.Handlers.LoadContentContext aContext)
{
// dynamically weld TPart to content item when condition is met (is a user, does record exist)
if (aContext.ContentItem.Is<Orchard.Users.Models.UserPart>())
{
if (!aContext.ContentItem.Is<TPart>())
{
if (mPartRecords.Count(r => r.Id == aContext.ContentItem.Id) > 0)
aContext.ContentItem.Weld<TPart>();
}
}
}
...
// private
Orchard.Data.IRepository<TRecord> mPartRecords;
}
3) Define the content handler for the dynamic content part
public abstract class BasePartHandler<TPart, TRecord> : Orchard.ContentManagement.Handlers.ContentHandler
where TPart: Orchard.ContentManagement.ContentPart<TRecord>, new()
where TRecord: Orchard.ContentManagement.Records.ContentPartRecord, new()
{
// public
// the constructor of a content handler is called when a content item (e.g. user) is created
public BasePartHandler(Orchard.Data.IRepository<TRecord> aPartRecords)
{
...
// add storage filter for dynamically welding TPart to content item
Filters.Add(new BaseWeldBeforeStorageFilter<TPart, TRecord>(aPartRecords));
// enable storing TPart to associated table
Filters.Add(Orchard.ContentManagement.Handlers.StorageFilter.For<TRecord>(aPartRecords));
...
// listen to user creation, update, removal...
OnCreated<Orchard.Users.Models.UserPart>(UserCreated);
...
}
...
// private
private void UserCreated(Orchard.ContentManagement.Handlers.CreateContentContext aContext, Orchard.Users.Models.UserPart aUserPart)
{
if (...) // condition for checking whether user
CreatePartRecordWhenNeededAndWeldPart(aContext.ContentItem, ...);
}
private void CreatePartRecordWhenNeededAndWeldPart(Orchard.ContentManagement.ContentItem aContentItem)
{
TPart lPart = aContentItem.Weld<TPart>();
// assign record, adopted from StorageFilter.cs
// todo: find a way to do it the "Orchard way" as this feels like hack
lPart._record.Loader(r =>
new TRecord {
Id = aContentItem.Id,
ContentItemRecord = new Orchard.ContentManagement.Records.ContentItemRecord {Id = aContentItem.Id}
});
// there are situations where part record already exists in DB but part is not welded at this point, thus check for existing record to avoid
// - creating record multiple times
// - NHibernate exception
if (!mPartRecords.Table.Contains(lPart.Record))
mPartRecords.Create(lPart.Record);
}
private Orchard.Data.IRepository<TRecord> mPartRecords;
}
As for now, the dynamic content part handling is working but I'm still unsure how to create a content part record in Orchard properly (see todo hint in source code of step 3).

Related

Is there a way to make custom lookup on dialog field on Microsoft Dynamics 365?

I have problems with my dialog field. I have button that opens dialog tab with field. It was supposed to show on that field lookup exact records(i guess i need select there by one field value). Right now i have this code:
DialogField Journal = dialog.addField(extendedTypeStr(JournalId));
This dialog line adds a field with all values on that EDT. I have 3 journal types - NEW, UPDATE, DELETE. Right now on that field lookup it shows me all 3 journal types. I want to make custom lookup that shows exact type , example - if i click that button on journal that has type "NEW", then it should show only "NEW" type of journal types on lookup. I heard there is something like dialog.addLookup or something. Can someone help me?
You already added your dialog field (in the dialog() method). Now add the dialogRunPost() method that is executed after the form GUI is initialized. At that point you can fetch the underlying FormStringControl behind the dialog field. Subscribing to the FormStringControl.OnLookup event allows you to override the lookup.
I did not have some journal data available, so I created a similar example with customers. My example dialog (MyDialog) takes a source customer (customerCaller) and shows a dialog with a custom lookup that only shows customers with the same customer group.
My example is also a standalone, runnable class and is not called from a form. Comments have been added to indicate where this affects the code.
Full example
public class MyDialog extends Runbase
{
// fields
protected Args args;
protected CustTable customerCaller;
protected DialogField dfCustomerId;
// construct
public static MyDialog newArgs(Args _args)
{
MyDialog ret = new MyDialog();
ret.args = _args;
return ret;
}
// initialize
public boolean init()
{
boolean ret = super();
// validate and fetch caller
if (args.record() && args.record().TableId == tableNum(CustTable))
//if (args.caller() && args.caller().dataset() == tableNum(CustTable)) --> when called from form
{
customerCaller = args.record();
//customerCaller = args.caller().record();
}
else
{
throw error(Error::missingRecord('My Dialog'));
}
return ret;
}
// build dialog
public Object dialog()
{
Dialog ret = super();
// optional reference to visualize the input
ret.addText('Caller customer group = ' + customerCaller.CustGroup);
// add field
dfCustomerId = ret.addField(extendedTypeStr(CustAccount)); // default lookup = all CustTable.AccountNum values
return ret;
}
public void dialogPostRun(DialogRunbase dialog)
{
super(dialog);
// subscribe to lookup event
FormStringControl fscCustomerId = dfCustomerId.control();
fscCustomerId .OnLookup += eventhandler(this.customerId_OnLookup);
}
// custom lookup for customer id
protected void customerId_OnLookup(FormControl _sender, FormControlEventArgs _e)
{
// cancel default
FormControlCancelableSuperEventArgs eventArgs = _e;
eventArgs.CancelSuperCall();
// define lookup query (list all customers with same customer group as input customer)
Query query = new Query();
QueryBuildDataSource qbds = SysQuery::findOrCreateDataSource(query, tableNum(CustTable));
SysQuery::findOrCreateRange(qbds, fieldNum(CustTable, CustGroup)).value(SysQuery::value(customerCaller.CustGroup));
// do lookup
SysTableLookup lookup = SysTableLookup::newParameters(tableNum(CustTable), _sender);
lookup.parmQuery(query);
lookup.addLookupfield(fieldNum(CustTable, AccountNum), true);
lookup.addLookupfield(fieldNum(CustTable, CustGroup));
lookup.performFormLookup();
}
// run dialog
public static void main(Args _args)
{
// I am running this dialog directly (not from a form), generating some random input
CustTable customer;
select firstonly customer where customer.CustGroup != '';
_args.record(customer);
// end of random input
MyDialog md = MyDialog::newArgs(_args);
md.init();
if (md.prompt())
{
md.run();
}
}
}
Result

Release invoice on new screen

I need your help.
I have created a new screen, where I am calling all invoices pending release.
I have problems to release, I send a message where you request (you want to release).
It shows me the infinite message.
Only once should you ask me, then you should go out and follow the normal process.
public ProcessDocNew()
{
// Acuminator disable once PX1008 LongOperationDelegateSynchronousExecution [Justification]
Document.SetProcessDelegate(
delegate (List<ARInvoice> list)
{
List<ARRegister> newlist = new List<ARRegister>(list.Count);
foreach (ARInvoice doc in list)
{
newlist.Add(doc);
}
ProcessDoc(newlist, true);
}
);
Document.SetProcessCaption(ActionsMensje.Process);
Document.SetProcessAllCaption(ActionsMensje.ProcessAll);
}
public virtual void ProcessDoc(List<ARRegister> list, bool isMassProcess)
{
string title = "Test";
string sms = "¿Stamp?";
var Graph = PXGraph.CreateInstance<ARInvoiceEntry>();
ARInvoice document = Document.Current;
PEFEStampDocument timbrar = new PEFEStampDocument();/*This is a class where it is, all my method*/
if (isMassProcess == true)
{
Document.Ask(title, sms, MessageButtons.YesNo, MessageIcon.Question);
{
PXLongOperation.StartOperation(Graph, delegate
{
timbrar.Stamp(document, Graph); /*here I have my release method*/
});
}
}
}
public static class ActionsMensje
{
public const string Process = "Process";
public const string ProcessAll = "Process All";
}
I await your comments
Only once should you ask me, then you should go out and follow the
normal process.
That is not how the processing pattern works. The process delegate is called for each record and is therefore not a valid location to display a message that should be shown only once.
You would need to add a custom action to achieve that behavior. The scenario you're looking for should be implemented with a processing filter checkbox and processing filter to comply with best practices:
Documentation on processing screens implementation is available here:
https://help-2019r2.acumatica.com/Help?ScreenId=ShowWiki&pageid=a007b57b-af69-4c0f-9fd1-f5d98351035f

DDD entity with complex creation process

How entities with complex creation process should be created in DDD? Example:
Entity
- Property 1
- Property 2: value depends on what was provided in Property 1
- Property 3: value depends on what was provided in Property 1
- Property 4: value depends on what was provided in Property 1, 2 and 3
I have two ideas but both looks terrible:
Create entity with invalid state
Move creation process to service
We are using REST API so in first scenario we will have to persist entity with invalid state and in second scenario we move logic outside of the entity.
You can use the Builder Pattern to solve this problem.
You can make a Builder that has the logic for the dependencies between properties and raise Exceptions, return errors or has a mechanism to tell the client which are the next valid steps.
If you are using an object orienterd language, the builder can also return different concrete classes based on the combination of these properties.
Here's a very simplified example. We will store a configuration for EventNotifications that can either listen on some Endpoint (IP, port) or poll.
enum Mode { None, Poll, ListenOnEndpoint }
public class EventListenerNotification {
public Mode Mode { get; set; }
public Interval PollInterval { get; set; }
public Endpoint Endpoint { get; set; }
}
public class Builder {
private Mode mMode = Mode.None;
private Interenal mInterval;
private Endpoint mEndpoint;
public Builder WithMode(Mode mode) {
this.mMode = mode;
return this;
}
public Builder WithInterval(Interval interval) {
VerifyModeIsSet();
verifyModeIsPoll();
this.mInterval = interval;
return this;
}
public Builder WithEndpoint(Endpoint endpoint) {
VerifyModeIsSet();
verifyModeIsListenOnEndpoint();
this.mInterval = interval;
return this;
}
public EventListenerNotification Build() {
VerifyState();
var entity = new EventListenerNotification();
entity.Mode = this.mMode;
entity.Interval = this.mInterval;
entity.Endpoint = this.mEndpoint;
return entity;
}
private void VerifyModeIsSet() {
if(this.mMode == Mode.None) {
throw new InvalidModeException("Set mode first");
}
}
private void VerifyModeIsPoll() {
if(this.mMode != Mode.Poll) {
throw new InvalidModeException("Mode should be poll");
}
}
private void VerifyModeIsListenForEvents() {
if(this.mMode != Mode.ListenForEvents) {
throw new InvalidModeException("Mode should be ListenForEvents");
}
}
private void VerifyState() {
// validate properties based on Mode
if(this.mMode == Mode.Poll) {
// validate interval
}
if(this.mMode == Mode.ListenForEvents) {
// validate Endpoint
}
}
}
enum BuildStatus { NotStarted, InProgress, Errored, Finished }
public class BuilderWithStatus {
private readonly List<Error> mErrors = new List<Error>();
public BuildStatus Status { get; private set; }
public IReadOnlyList<Error> Errors { get { return mErrors; } }
public BuilderWithStatus WithInterval(Interval inerval) {
if(this.mMode != Mode.Poll) {
this.mErrors.add(new Error("Mode should be poll");
this.Status = BuildStatus.Errored;
}
else {
this.mInterval = interval;
}
return this;
}
// rest is same as above, but instead of throwing errors you can record the error
// and set a status
}
Here are some resources with more information and other machisms that you can use:
https://martinfowler.com/articles/replaceThrowWithNotification.html
https://martinfowler.com/eaaDev/Notification.html
https://martinfowler.com/bliki/ContextualValidation.html
Take a look at chapter 6 of the Evans book, which specifically talks about the life cycle of entities in the domain model
Creation is usually handled with a factory, which is to say a function that accepts data as input and returns a reference to an entity.
in second scenario we move logic outside of the entity.
The simplest answer is for the "factory" to be some method associate with the entity's class - ie, the constructor, or some other static method that is still part of the definition of the entity in the domain model.
But problem is that creation of the entity requires several steps.
OK, so what you have is a protocol, which is to say a state machine, where you collect information from the outside world, and eventually emit a new entity.
The instance of the state machine, with the data that it has collected, is also an entity.
For example, creating an actionable order might require a list of items, and shipping addresses, and billing information. But we don't necessarily need to collect all of that information at the same time - we can get a little bit now, and remember it, and then later when we have all of the information, we emit the submitted order.
It may take some care with the domain language to distinguish the tracking entity from the finished entity (which itself is probably an input to another state machine....)

Orchard Custom Workflow Activity

I have built a custom module in Orchard that creates a new part, type and a custom activity but I'm struggling with the last part of what I need to do which is to create a copy of all the content items associated with a specific parent item.
For instance, when someone creates a "Trade Show" (new type from my module), various subpages can be created off of it (directions, vendor maps, etc.) since the client runs a single show at a time. What I need to do is, when they create a new Trade Show, I want to get the most recent prior show (which I'm doing via _contentManager.HqlQuery().ForType("TradeShow").ForVersion(VersionOptions.Latest).ForVersion(VersionOptions.Published).List().Last() (positive that's not the most efficient way, but it works and the record count would be ~10 after five years), then find all of those child pages that correlate to that old show and copy them into new Content Items. They have to be a copy because on occasion they may have to refer back to parts with the old shows, or it could change, etc. All the usual reasons.
How do I go about finding all of the content items that reference that prior show in an Activity? Here is my full class for the Activity:
using System;
using System.Collections.Generic;
using System.Linq;
using Orchard.Autoroute.Services;
using Orchard.ContentManagement;
using Orchard.Localization;
using Orchard.Projections.Models;
using Orchard.Projections.Services;
using Orchard.Workflows.Models;
using Orchard.Workflows.Services;
using Orchard.Workflows.Activities;
namespace Orchard.Web.Modules.TradeShows.Activities
{
public class TradeShowPublishedActivity : Task
{
private readonly IContentManager _contentManager;
private readonly IAutorouteService _autorouteService;
private readonly IProjectionManager _projectionManager;
public TradeShowPublishedActivity(IContentManager contentManager, IAutorouteService autorouteService, IProjectionManager projectionManager)
{
_contentManager = contentManager;
_autorouteService = autorouteService;
_projectionManager = projectionManager;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public override LocalizedString Category
{
get { return T("Flow"); }
}
public override LocalizedString Description
{
get { return T("Handles the automatic creation of content pages for the new show."); }
}
public override string Name
{
get { return "TradeShowPublished"; }
}
public override string Form
{
get { return null; }
}
public override IEnumerable<LocalizedString> GetPossibleOutcomes(WorkflowContext workflowContext, ActivityContext activityContext)
{
yield return T("Done");
}
public override IEnumerable<LocalizedString> Execute(WorkflowContext workflowContext, ActivityContext activityContext)
{
var priorShow = _contentManager.HqlQuery().ForType("TradeShow").ForVersion(VersionOptions.Latest).ForVersion(VersionOptions.Published).List().Last();
var tradeShowPart = priorShow.Parts.Where(p => p.PartDefinition.Name == "TradeShowContentPart").Single();
//new show alias
//workflowContext.Content.ContentItem.As<Orchard.Autoroute.Models.AutoroutePart>().DisplayAlias
yield return T("Done");
}
}
}
My Migrations.cs file sets up the part that is used for child pages to reference the parent show like this:
ContentDefinitionManager.AlterPartDefinition("AssociatedTradeShowPart", builder => builder.WithField("Trade Show", cfg => cfg.OfType("ContentPickerField")
.WithDisplayName("Trade Show")
.WithSetting("ContentPickerFieldSettings.Attachable", "true")
.WithSetting("ContentPickerFieldSettings.Description", "Select the trade show this item is for.")
.WithSetting("ContentPickerFieldSettings.Required", "true")
.WithSetting("ContentPickerFieldSettings.DisplayedContentTypes", "TradeShow")
.WithSetting("ContentPickerFieldSettings.Multiple", "false")
.WithSetting("ContentPickerFieldSettings.ShowContentTab", "true")));
Then, my child pages (only one for now, but plenty more coming) are created like this:
ContentDefinitionManager.AlterTypeDefinition("ShowDirections", cfg => cfg.DisplayedAs("Show Directions")
.WithPart("AutoroutePart", builder => builder.WithSetting("AutorouteSettings.AllowCustomPattern", "true")
.WithSetting("AutorouteSettings.AutomaticAdjustmentOnEdit", "false")
.WithSetting("AutorouteSettings.PatternDefinitions", "[{Name:'Title', Pattern: '{Content.Slug}', Description: 'international-trade-show'}]")
.WithSetting("AutorouteSettings.DefaultPatternIndex", "0"))
.WithPart("CommonPart", builder => builder.WithSetting("DateEditorSettings.ShowDateEditor", "false"))
.WithPart("PublishLaterPart")
.WithPart("TitlePart")
.WithPart("AssociatedTradeShowPart") /* allows linking to parent show */
.WithPart("ContainablePart", builder => builder.WithSetting("ContainablePartSettings.ShowContainerPicker", "true"))
.WithPart("BodyPart"));
So you have the Trade Show content item, the next step will be to find all parts with a ContentPickerField, then filter that list down to those where the field contains your show's ID.
var items = _contentManager.Query().List().ToList() // Select all content items
.Select(p => (p.Parts
// Select all parts on content items
.Where(f => f.Fields.Where(d =>
d.FieldDefinition.Name == typeof(ContentPickerField).Name &&
// See if any of the fields are ContentPickerFields
(d as ContentPickerField).Ids.ToList().Contains(priorShow.Id)).Any())));
// That field contains the Id of the show
This could get expensive depending on how many content items are in your database.

Orchard alternates based on Tag

I want to create alternates for content item based on its tag value.
For example, I want to create an alternate called List-ProjectionPage-tags-special
Searching the nets directs me to implement a new ShapeDisplayEvents
Thus, I have
public class TagAlternatesFactory : ShapeDisplayEvents
{
public TagAlternatesFactory()
{
}
public override void Displaying(ShapeDisplayingContext context)
{
}
}
In the Displaying method, I believe I need to check the contentItem off the context.Shape and create an alternate name based off of that (assuming it has the TagsPart added to the content item).
However, what do I do with it then? How do I add the name of the alternate? And is that all that's needed to create a new alternate type? Will orchard know to look for List-ProjectionPage-tags-special?
I took a cue from Bertrand's comment and looked at some Orchard source for direction.
Here's my implementation:
public class TagAlternatesFactory : ShapeDisplayEvents
{
public override void Displaying(ShapeDisplayingContext context)
{
context.ShapeMetadata.OnDisplaying(displayedContext =>
{
var contentItem = displayedContext.Shape.ContentItem;
var contentType = contentItem.ContentType;
var parts = contentItem.Parts as IEnumerable<ContentPart>;
if (parts == null) return;
var tagsPart = parts.FirstOrDefault(part => part is TagsPart) as TagsPart;
if (tagsPart == null) return;
foreach (var tag in tagsPart.CurrentTags)
{
displayedContext.ShapeMetadata.Alternates.Add(
String.Format("{0}__{1}__{2}__{3}",
displayedContext.ShapeMetadata.Type, (string)contentType, "tag", tag.TagName)); //See update
}
});
}
}
This allows an alternate view based on a tag value. So, if you have a project page that you want to apply a specific style to, you can simply create your alternate view with the name ProjectionPage_tag_special and anytime you want a projection page to use it, just add the special tag to it.
Update
I added the displayedContext.ShapeMetadata.Type to the alternate name so specific shapes could be overridden (like the List-ProjectionPage)

Resources