umco / umbraco-ditto

Ditto - the friendly view-model mapper for Umbraco

Home Page:http://our.umbraco.org/projects/developer-tools/ditto

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Ability to Conditionally Stop Processor Chain

Nicholas-Westby opened this issue · comments

It would be neat if I could do this:

[GetMyTranslatedText(Term = "Image Gallery Load More Text")]
[StopIfNotNull]
[GetMyTranslatedText(Term = "Fallback Load More Text")]
public string LoadMoreText { get; set; }

The way I imagining this working is that each processor would run in sequence, but the StopIfNotNullAttribute processor would have the ability to prevent the second GetMyTranslatedTextAttribute processor from running if the result of the previous processor was not null. If the previous processor was null, the remaining processor can still run to fetch a fallback value.

One way this could work would be if there were an ExitEarly property on the processor. If set to true, the subsequent processors would not be called.

I realize there are other ways to accomplish this particular fallback mechanism in this instance, but this is really just a contrived example so I can explain the concept in simple terms.

@Nicholas-Westby Thanks for the suggestion. I've been thinking over it for the past day, trying to decide if this is too much of an edge-case or not... but the more I think about it, I'm coming around to the idea of having an Exit property available on the base DittoProcessorAttribute would work - then we could break out the processors loop.

I can't decide if Exit or ExitEarly is the correct term for it, (grr, naming things is hard :neckbeard:)

@mattbrailsford Any thoughts?

Glad to hear you're open to the idea :-)

Another idea would be "BreakChain" or, more verbosely, "BreakProcessorChain". Or "StopChain", "StopChaining", "HaltProcessing", "InterruptProcessing", "SkipChain", "SkipRemaining" (I'm sure you can imagine the other variations).

Actually, now that I think about "Skip", you could even allow for skipping a certain number of subsequent processors, which would allow for a fallback, but then a number of subsequent operations (aka, processors) to run based on either the main value or the fallback value.

Imagine a media gallery which can contain either images or videos. This media gallery only shows 9 items to start with and has a "View More" button that loads more media items (i.e., more images or more videos). Imagine further that the text on the button changes depending on current language and on whether it's an image gallery or video gallery. Here's what the processor chain might look like for the property used for that button's text:

// Gallery type is a property that can be "Image" or "Video".
[UmbracoProperty("galleryType")]
// For example, could return "Media Gallery - View More Text - Image".
[ConstructTranslationTerm(TermPrefix = "Media Gallery - View More Text - ")]
// Attempt to translate the term (e.g., "Media Gallery - View More Text - Image")
// into the current language. Might return null if no translation exists.
[DictionaryTranslation]
// Skips the subsequent 2 processors if the translation was successful.
[SkipProcessorsIfNotNull(2)]
// Just returns the string "General - View More Text"]
[StringValue("General - View More Text")]
[DictionaryTranslation]
// This would translate "Click to " into the current language, then it would
// perform a prefix operation to convert "View More Images" to "Click to View More Images"
// (assuming the current language is English).
[PrefixTranslatedString("Click to ")]
// If for some reason the above processors fail to return a non-empty string,
// throw an exception as we really, really want this string to be non-empty.
[ExceptionIfNullOrEmpty]
public string ViewMoreText { get; set; }

In essence, this is trying to get the best translation it can for the text. It's trying the specialized translation first (e.g., "View More Images"), but falling back to the generic translation as a fallback (e.g., "View More").

The end result of this processor chain would be to set the ViewMoreText property to a string of "Click to View More Images", but it would depend on which translations were available, and which gallery type was specified.

Hi, don't you need to put an Order attribute to allow processors to run sequentially?

@garpunkal it helps! :)

@mattbrailsford What do you imagine the processor chain looking like for the solution you propose?

@garpunkal The order property is not really necessary in practice. It's good to apply it "just in case", as attribute order is not guaranteed, though on the .Net implementation I work with it seems to retain order in practice. Hypothetically, I could deploy to a different machine with a different .Net implementation (e.g., Mono) that may not retain the order (also, a future version of .Net may ignore the order too, so an upgrade could cause things to start to fail).

cool, I thought you did! I normally apply it just in-case, but obviously it bulks out the attributes. It's a shame the order isn't guaranteed.

In the same approach as entity framework has a fluent API ever been discussed to get around the ordering of attributes?

@mattbrailsford The original post contains a very small example of the "stop processor" functionality. The later example is for "skip processor" functionality.

Are you asking for a small example of the "skip processor" functionality?

I started prototyping out a way to stop/exit the processor chain, but I hit a snag - in that it would also prevent the post-processors from running too. (The post-processors are required for any final casting/conversions). Meaning that the final processor would need to return the target value's exact type... it could get problematic and may need us to refactor processor execution.

re: Skip next x processors feature - I'm not convinced on this yet, feels edge-case again, (sorry). Personally, I'd be looking making use of custom processors and grouping frequently used processors together within DittoMultiProcessors.

Ok, so looking back at your example @Nicholas-Westby #203 (comment), I think you may be getting too granular with your processors. I'd be tempted to do something more like this:

[UmbracoProperty("galleryType")]
[FormattedDictionary(Key = "Media Gallery - View More Text - {0}", AltKey = "General - View More Text", Prefix = "Click to ")]

And have the FormattedDictionaryProcessor contain the logic to format the key (using current Value as the param), check for a dictionary value, and if one doesn't exist, use the AltKey instead. Finally, before returning, if a prefix exists, prefix your value.

KISS

Gonna close this issue down for now as I think you can negate the need for a breakout by better designing your processors. @Nicholas-Westby if you feel there is a better example, please feel free to post it and re-open the issue and we can re-review it.

@mattbrailsford I've moved the conversation to a forum thread: https://our.umbraco.org/projects/developer-tools/ditto/ditto-feedback/82478-manually-running-processors-from-dittomultiprocessor

TLDR: I'm not sure how to collapse multiple processors into a single processor that is then able to conditionally run some of the processors.

@Nicholas-Westby I'm not sure why you've moved it to our and then reference back to this issuse, why not keep it all together? Additionally, the example you've given on our is not the same examples here so it's hard to give you concrete answers when the goal posts keep moving.

I've given an example above of how to handle your dictionary based flow, does this not answer your question?

@mattbrailsford The forum post is a question, whereas this thread is regarding a feature request. Doesn't feel appropriate to ask a question in the issue tracker.

My request was never the ability to handle dictionary based flow. That was just a contrived example to demonstrate the need. In more general terms, the use case is the ability to fallback to different values based on some condition. The proposed general solution was to provide the ability to stop the processor chain early. A further generalized solution which may service additional use cases would be to provide the ability to skip a certain number of processors depending on some condition.

Rather than proposing solutions and giving contrived examples, I can instead restate the desire (in more general terms) and leave it to you to recommend any solution you might deem appropriate. Here it is:

I'd like to be able to run processors in a non-linear fashion in order to ensure reusability while retaining enough flexibility that I can get values appropriate to my needs, including the occasional need to fallback to more generic values when a specific value is not available.

Note the term "non-linear" may not be quite accurate, but I can't think of a better term for "may vary depending on the input and output of some operations".

The problem I am noticing with processors is that they are black boxes that can't necessarily be reused in flexible scenarios. They can to some extent be reused with a DittoMultiProcessor, but as far as I've seen that only allows the simplest of chaining (i.e., no conditions in the chain, no skipping parts of the chain, no early exits from the chain).

This solution that you've proposed:

[UmbracoProperty("galleryType")]
[FormattedDictionary(Key = "Media Gallery - View More Text - {0}", AltKey = "General - View More Text", Prefix = "Click to ")]

Seems to make at least one assumption. For example, it assumes that I have access to the code in the processors it replaces. What if ConstructTranslationTerm or DictionaryTranslation come from a library of Ditto processors and reconstructing what they do would be very time consuming? Unless your FormattedDictionary calls the other processors in a flexible way (i.e., with conditions), I don't see how it satisfies the request.

If it helps narrow down this request and determine if this feature is required or not, it can stay here as part of this issue until a decision is made, but I understand your reasoning.

Ok, well, if we are only dealing with general examples, then all I can do is point you to the various tools in the toolbelt.

Off the bat, the first is to state again that processors should first check the value and ensure it can run before attempting to do so, so for example, maybe the dictionary processors should only run if the passed in value is null or empty, in this case, if the first one ran and returned a value, the second instance shouldn't need to run and thus just passes the Value back out.

Second, we have processor contexts. If you need your processor to respond to a UI input, then the value should be put into a custom processor context and your processor should use it and make it's decisions based upon those values inside the processor.

Lastly, the two options you've got for wrapping an existing processor is either a MultiProcessor (this is generally more used for cases where you tend to run a few processors along side each other constantly and to streamline your code) or by extending a processor, so for example, maybe my FormattedDictionary attribute extends the built in Dictionary processor and it just handles the Formatting of the string and passes the key onto the inherited Dictionary processor.

My general thought based on your examples is that you are just creating too granular processors. Chaining is inherently linear, so if you need to do multiple checks, you are probably better of wrapping that in a processor.

The only other thing I can think to do if you want to run a non linear set of processors is to create something like a factory processor who's job is to return the processors to run, but you'd have to code this yourself (probably take a look at the MultiProcessorAtribute for how it runs it's inner processors).

In all cases, I don't think the need to break out or stop a chain early, or skip some chain steps is needed. It can always be done by a more linear and logic means, so I would push you down these routes before attempting this.

Hopefully that gives you enough info as to why I don't think this is warranted, but if you do have an example that just can't be handled any other way, feel free to post it here.

Cool. If I think up another scenario where I need this, I'll let you know.