[Microstation CE u13] ElementGraphicsProcessor example in C#

Hi all,

I am trying to extract all of the elements contained within a paramteric cell, and it was suggested that I use the ElementGraphicsProcessor abstract class to achieve this. The objective is to to return all graphical elements from the cell instance in their true location (with the same geometry), along with their associated symbology. Essentially this would have a similar outcome as dropping the cell however the typical workflow for dropping an element programmatically doesn't seem to work on parametric cells unfortunately. Does anyone have any examples or documentation that could enlighten me on how to achieve this?

Thanks!


Ed

Parents
  • Hi Edward,

    and it was suggested that I use the ElementGraphicsProcessor abstract class to achieve this

    Er ... no, in fact. I recommend to use ElementGraphicsProcessor to obtain geometry, not elements! It's important to distinguish between these two different information ;-)

    The objective is to to return all graphical elements from the cell instance in their true location (with the same geometry)

    In general, some objects allow to access internal elements, some not. There is even a method called ExposeChildren to determine whether this functionality is supported. My feeling is that general concept of new API is to do not expose internal elements (as elements), because it's implementation / persistence detail, but to prefer to use the object geometry everywhere it's possible.

    Whereas e.g. plain cell is the example of the element exposing children, parametric cell is black bock and it's not possible to access original elements (at lest in the current API version). What is not clear whether it's "by design" or it's just "not implemented yet" feature, because constraints-based modeling is still under development and new features are added.

    the typical workflow for dropping an element programmatically doesn't seem to work on parametric cells unfortunately

    I think the drop tool does not work well with parametric cells in general, not only in API.

    Does anyone have any examples or documentation that could enlighten me on how to achieve this?

    There is, the processor is used in some example delivered with SDK and when you will search this forum, you will find discussion about different aspects of this (great and clever!) object ... also because it seems in some situations the wrapper around native processor does not work as expected.

    The code can be similar to this one:

        internal class Demo
        {
            public void IterateAllElements()
            {
                ParametricCellProcessor processor = new ParametricCellProcessor();
    
                DgnModel model = Session.Instance.GetActiveDgnModel();
    
                ModelElementsCollection elements = model.GetGraphicElements();
    
                foreach (Element element in elements)
                {
                    if (element is ParametricCellElement instance)
                    {
                        MessageCenter.Instance.StatusMessage = $"Parametric cell: {instance.CellDefinition.CellName}";
                        ElementGraphicsOutput.Process(instance, processor);
                    }
                }
            }
        }
    
        internal class ParametricCellProcessor : ElementGraphicsProcessor
        {
            public override bool ProcessAsBody(bool isCurved)
            {
                return false;
            }
    
            public override bool ProcessAsFacets(bool isPolyface)
            {
                return false;
            }
    
            public override BentleyStatus ProcessCurveVector(CurveVector curves, bool isFilled)
            {
                return BentleyStatus.Error;
            }
    
            public override BentleyStatus ProcessCurvePrimitive(CurvePrimitive curve, bool isClosed, bool isFilled)
            {
                MessageCenter.Instance.StatusMessage = $"{curve.GetCurvePrimitiveType()}";
                return BentleyStatus.Success;
            }
    
            public override bool WantClipping()
            {
                return false;
            }
        }

    I recommend (in fact, I think you have to) to read ElementGraphicsProcessor description in native MicroStatioAPI doc, because it's crucial to understand what "bool returning" methods should be overridden to obtain what result.

    When you are interested in symbology (or other "not geometry info), override proper AnnounceXYZ method.

    With regards,

      Jan

  • Thanks Jan,

    use ElementGraphicsProcessor to obtain geometry, not elements!

    You're right, poor choice of words! I need to obtain the geometry of the parametric cell in order to build the (new) elements.

    In general, some objects allow to access internal elements, some not. There is even a method called ExposeChildren to determine whether this functionality is supported

    This is actually what I attempted at first, but quickly realised that there were no children that could be exposed within a Parametric Cell Element. If only it were that easy!

    I think the drop tool does not work well with parametric cells in general, not only in API

    If attempting to do this manually using the 'Drop Element' tool with just the 'Application Elements' ticked everything works as I'd hoped it would, dropping them to their original elements:

    Which makes me wonder if perhaps their is a simpler method to drop Application Elements to their primitives/breps?

    I recommend (in fact, I think you have to) to read ElementGraphicsProcessor description in native MicroStatioAPI doc, because it's crucial to understand what "bool returning" methods should be overridden to obtain what result

    Thanks for the code sample, I'll test that out. With regards to the documentation; when reading through the API docs and forum posts, I didn't quite understand how the geometry is returned to the main script. Am I right in assuming I should read the geometry output from the MessageCenter and use that info to build all the required geometry, through the use of Bentley.GeometryNET library? Looking through the GeometryNET library there are a vast array of methods for each element type; and it looks like a very arduous task if I need to write a script to use the correct methods for drawing an arc, line etc. for all the possible permutations that may occur! Hopefully this is not the case and my interpretation is wrong... What seemed like an easy task in order to finish my GC Node has turned into a full day of struggling to get nowhere lol.
    I really appreciate all the advice to help make sense of this!

    Thanks,

    Ed

Reply
  • Thanks Jan,

    use ElementGraphicsProcessor to obtain geometry, not elements!

    You're right, poor choice of words! I need to obtain the geometry of the parametric cell in order to build the (new) elements.

    In general, some objects allow to access internal elements, some not. There is even a method called ExposeChildren to determine whether this functionality is supported

    This is actually what I attempted at first, but quickly realised that there were no children that could be exposed within a Parametric Cell Element. If only it were that easy!

    I think the drop tool does not work well with parametric cells in general, not only in API

    If attempting to do this manually using the 'Drop Element' tool with just the 'Application Elements' ticked everything works as I'd hoped it would, dropping them to their original elements:

    Which makes me wonder if perhaps their is a simpler method to drop Application Elements to their primitives/breps?

    I recommend (in fact, I think you have to) to read ElementGraphicsProcessor description in native MicroStatioAPI doc, because it's crucial to understand what "bool returning" methods should be overridden to obtain what result

    Thanks for the code sample, I'll test that out. With regards to the documentation; when reading through the API docs and forum posts, I didn't quite understand how the geometry is returned to the main script. Am I right in assuming I should read the geometry output from the MessageCenter and use that info to build all the required geometry, through the use of Bentley.GeometryNET library? Looking through the GeometryNET library there are a vast array of methods for each element type; and it looks like a very arduous task if I need to write a script to use the correct methods for drawing an arc, line etc. for all the possible permutations that may occur! Hopefully this is not the case and my interpretation is wrong... What seemed like an easy task in order to finish my GC Node has turned into a full day of struggling to get nowhere lol.
    I really appreciate all the advice to help make sense of this!

    Thanks,

    Ed

Children
  • If only it were that easy!

    Maybe easy for developers, but wrong in terms of data structures.

    It's my subjective feeling, but I see a movement from "C data structures" to "object abstraction using handlers (services)". Whereas it was normal in DGN V7 and also in V8 times to go inside structures and to change directly different parts (which often ended in strange or even corrupted elements), now it's more like "here is a handler, call services (queries) that you need, but do not care about implementation details".

    As written in C++ documentation, every handler should be very careful about providing a list of children elements, because in such case the handler has not any control over them. So for some objects it's possible, for some not.

    If attempting to do this manually using the 'Drop Element' tool with just the 'Application Elements' ticked everything works as I'd hoped it would, dropping them to their original elements:

    I guess there is no problem with 2D constraints, but I tried it with some 3D objects and the results were bad.

    Which makes me wonder if perhaps their is a simpler method to drop Application Elements to their primitives/breps?

    There is Drop structure available in C++, but I think it's not available in managed API (yet?).

    I didn't quite understand how the geometry is returned to the main script.

    I am not sure what you are asking about.

    What processor method is called and how (in what format) the geometry is provided depends on the processor configuration (what type of geometry are you interested in).

    What you will do next with received geometry is completely up to you (e.g. list of geometries added to a list injected to processor?) and does not relate to API itself.

    Looking through the GeometryNET library

    Why? At first, look at examples delivered with MicroStation SDK, at second, search in this forum, at third, read this community blogs (even when there is not many articles). In my opinion, after several years of CE API existence, all typical activities / workflows have been discussed and snippets provided.

    and use that info to build all the required geometry, through the use of Bentley.GeometryNET library?

    Not at all. GeometryNET library is mostly internal aspect of API and usually there is no reason to use it for anything (only to reference the assembly when required).

    To create elements from existing topology / geometry data, use DraftinElementSchema class.

    and it looks like a very arduous task if I need to write a script to use the correct methods for drawing an arc, line etc. for all the possible permutations that may occur!

    If your code ends with such state, something is probably wrong and it means you do not use OOP abstraction provided by API properly. For many requirements, specialized (single focused) class probably exists in API.

    What seemed like an easy task in order to finish my GC Node has turned into a full day of struggling to get nowhere lol.

    Sorry, but such comparison is complete nonsense in my opinion. There is no reason compare platform MicroStation NET API and specialized GC environment. They are different tools developed for different purposes.

    GC were implemented using platform API and it's why the platform API exists: Namely C++ API is ready to solve nearly any requirement and allows to implement nearly anything including all other discipline-specific product APIs. The low-level and flexible API typically requires to write more code ... similarly when you will try to compare C++ (powerful but seriously complicated) and Python (more user oriented and not so flexible, but more friendly).

    With regards,

      Jan

  • I need to obtain the geometry of the parametric cell in order to build the (new) elements.

    One more comment: You should be aware that using ElementGraphicsProcessor, it's not ensured you will be able to create exact copy of the source element. For 2D elements, you can expect that a curveVector equals to source element, but in the case of ParametricCell its structure is variable controlled by values.

    DropGeometry struct available in C++ API is closer to a concept of "splitting source element to individual children".

    So in summary, there are at least 3 different concepts (methods or classes) available:

    • GetChildren ... when it's supported
    • DropGeometry ... probably not available in NET now
    • ElementGraphicsProcessor

    For complete overview, see Geometry Collectors chapter in C++ API documentation.

    Regards,

      Jan

  • What seemed like an easy task in order to finish my GC Node has turned into a full day of struggling to get nowhere lol.

    Another reason why it seems to be so complicated is that you choose to work with the most complex element in MicroStation, that is, moreover, still under development (new features like different constrains have been discussed). And it's not very good idea to invest into API opening until everything is fixed and stable, so often it ends by a question "Why <something> is not possible?".

    Regards,

      Jan

  • If attempting to do this manually using the 'Drop Element' tool with just the 'Application Elements' ticked everything works as I'd hoped it would, dropping them to their original elements:

    Jumping in to comment on this; the "original element" is the application element, drop does not return original elements. Instead drop creates brand new elements from the application element's geometry.

    Dropping application elements is essentially an ElementGraphicsProcessor that calls the appropriate DraftingElementSchema::ToElement for each _Process callback.

    HTH

    -B



  • Thanks Jan,

    What you will do next with received geometry is completely up to you (e.g. list of geometries added to a list injected to processor?) and does not relate to API itself.

    Sorry I should have been more specific; I can see that the processor gathers all the geometry information but how do I then enumerate the results / pass this as a variable to DraftingElementSchema.ToElement/s? Please bear in mind I am very new to all of this, I've only had the SDK for a few weeks so I am finding things out the hard way ;)

    Code I have so far:

            public NodeUpdateResult DropParametricCell
            (
                NodeUpdateContext updateContext,
                [Replicatable, DgnModelProvider] object ParametricCell
            )
            {
                try
                {
                    Feature feat = ParametricCell as Feature;
                    long eleRef = feat.GetElement().MdlElementRef(); //Get Element Ref of GC Feature as long
                    Bentley.DgnPlatformNET.Elements.Element ele = Bentley.DgnPlatformNET.Elements.Element.GetFromElementRef((IntPtr)eleRef); //Retrieve Element as DgnPlatformNET Element
    
                    DgnModel model = Session.Instance.GetActiveDgnModel();
                    ParametricCellProcessor processor = new ParametricCellProcessor();
    
                    if(ele is ParametricCellElement instance)
                    {
                        Bentley.MstnPlatformNET.MessageCenter.Instance.StatusMessage = $"Parametric cell: {instance.CellDefinition.CellName}";
                        ElementGraphicsOutput.Process(instance, processor);
                        //What do I do now?!
                        //DraftingElementSchema.ToElement(model, , null);
                    }
                }
                catch (Exception ex)
                {
                    return new NodeUpdateResult.TechniqueException(ex);
                }
                return NodeUpdateResult.Success;
            }
        } // class
    
        internal class ParametricCellProcessor : ElementGraphicsProcessor
        {
            public override bool ProcessAsBody(bool isCurved)
            {
                return false;
            }
    
            public override bool ProcessAsFacets(bool isPolyface)
            {
                return false;
            }
    
            public override BentleyStatus ProcessCurveVector(CurveVector curves, bool isFilled)
            {
                return BentleyStatus.Error;
            }
    
            public override BentleyStatus ProcessCurvePrimitive(CurvePrimitive curve, bool isClosed, bool isFilled)
            {
                Bentley.MstnPlatformNET.MessageCenter.Instance.StatusMessage = $"{curve.GetCurvePrimitiveType()}";
                return BentleyStatus.Success;
            }
    
            public override bool WantClipping()
            {
                return false;
            }
        }
    } // namespace

    Thanks for all the help getting me this far!

    Edit - I think I finally understand what is required to pass the geometry to the main script. Do I need to add a List to collect the elements within the processor? The issue now is that I do not know what to pass to the DraftingElementSchema for the template element. Updated code:

            public NodeUpdateResult DropParametricCell
            (
                NodeUpdateContext updateContext,
                [Replicatable, DgnModelProvider] object ParametricCell
            )
            {
                try
                {
                    Feature feat = ParametricCell as Feature;
                    long eleRef = feat.GetElement().MdlElementRef(); //Get Element Ref of GC Feature as long
                    Bentley.DgnPlatformNET.Elements.Element ele = Bentley.DgnPlatformNET.Elements.Element.GetFromElementRef((IntPtr)eleRef); //Retrieve Element as DgnPlatformNET Element
    
                    DgnModel model = Session.Instance.GetActiveDgnModel();
                    ParametricCellProcessor processor = new ParametricCellProcessor();
    
                    if(ele is ParametricCellElement instance)
                    {
                        Bentley.MstnPlatformNET.MessageCenter.Instance.StatusMessage = $"Parametric cell: {instance.CellDefinition.CellName}";
                        
                        ElementGraphicsOutput.Process(instance, processor);
    
                        for (int i = 0; i < processor.curvePrimitives.Count; ++i)
                        {
                            Bentley.DgnPlatformNET.Elements.Element nele = DraftingElementSchema.ToElement(model, processor.curvePrimitives[i], null); //Currently throws an error...
                            nele.AddToModel();
                            Bentley.Interop.MicroStationDGN.Element fetch = MSApp.ActiveModelReference.GetLastValidGraphicalElement();
                            SetElement(fetch);
                            nele.Dispose();
                        }
                    }
                    
    
                }
                catch (Exception ex)
                {
                    return new NodeUpdateResult.TechniqueException(ex);
                }
    
                return NodeUpdateResult.Success;
            }
        } // class
    
        public class ParametricCellProcessor : ElementGraphicsProcessor
        {
            public List<CurvePrimitive> curvePrimitives = new List<CurvePrimitive>();
    
            public override bool ProcessAsBody(bool isCurved)
            {
                return false;
            }
    
            public override bool ProcessAsFacets(bool isPolyface)
            {
                return false;
            }
    
            public override BentleyStatus ProcessCurveVector(CurveVector curves, bool isFilled)
            {
                return BentleyStatus.Error;
            }
    
            public override BentleyStatus ProcessCurvePrimitive(CurvePrimitive curve, bool isClosed, bool isFilled)
            {
                Bentley.MstnPlatformNET.MessageCenter.Instance.StatusMessage = $"{curve.GetCurvePrimitiveType()}";
                curvePrimitives.Add(curve);
                
                return BentleyStatus.Success;
            }
    
            public override bool WantClipping()
            {
                return false;
            }
        }
    

    It throws an error on line 24, most likely due to null value for template. The list count = 8 though so it seems to be collecting something; when looking through debug values it is a heap of nested lists though and I couldn't see any raw geometry. Might be in there somewhere!

  • Hi Brian,
    Thanks for the insight, good to know dropping is a similar process. Do you happen to have any examples in C# for the DraftingElementSchema? I think I might be on the right track now but not sure what to set for the "Template Element":

    Element nele = DraftingElementSchema.ToElement(model, processor.curvePrimitives[i], //NOT SURE WHAT GOES HERE!);

  • Hi Ed,

    Do you happen to have any examples in C# for the DraftingElementSchema?

    Seriously? With all respect, it sounds to me it's easier for you to ask then to search anywhere:

    • I see 3 examples where DraftingElementSchema is used i MicroStation SDK examples
    • I received 67 results searching for DraftinElementSchema on this web

    Isn't it enough?

    BTW I do not differentiate between C++ and C# usage, because both API provides the same class, the only difference is that native object provides wider functionality.

    but not sure what to set for the "Template Element":

    I guess this rule has existed from V8.0 APIs (VBA, C, Interop...): When there is no element that should be used as a template available, use null.

    BTW When you are not sure and a particular parameter is not explained well in NET nor native documentation, use assembly decompiler to learn how NET wrapper is implemented. Very often it helps with the understanding at least a bit.

    With regards,

      Jan

  • Hi Ed,

    Please bear in mind I am very new to all of this

    New to what? One thing is start with MicroStation NET API, which can be confusing, but now much better knowledge base (examples, blogs, discussion) is available than when CE was released. But sometimes my feeling is that for people the problem is not in new API, but in weak knowledge of basic OOP concepts (SOLID etc.).

    but how do I then enumerate the results / pass this as a variable to DraftingElementSchema.

    In my opinion dependency injection is the solution here.

    Do I need to add a List to collect the elements within the processor?

    It's up to you what is your intention and also what software architecture do you prefer (because it can be implemented in several ways). Also, do not forget that Element is IDisposable, so you have to ensure it will be disposed explicitly.

    It throws an error on line 24, most likely due to null value for template.

    Absolutely not! When you say "error", it's like silence. What error? Is an exception thrown? When yes, what exception?

    Updated code:

    There are some ugliness:

    • What fashion (because I do not know about any reason) I have seen in many shared snippets to pollute code with full classes names (including namespaces)?
    • I do not know GC API, so I am not sure what is the best way, but to use ModelRef to obtain element plus using explicit casting (which potentially can ends with casting exception) looks not very elegant.

    This should work (at least MicroStation part), but be aware it's a concept, the structure is not fine (especially how elements are disposed):

    public NodeUpdateResult DropParametricCell
        (
            NodeUpdateContext updateContext,
            [Replicatable, DgnModelProvider] object ParametricCell
        )
        {
            try
            {
                Feature feat = ParametricCell as Feature;
                long id = feat.Element.ID;
    
                DgnModel model = Session.Instance.GetActiveDgnModel();
                Element element = dgnModel.FindElementById(new ElementId(ref id));
                
                ParametricCellProcessor processor = new ParametricCellProcessor();
    
    
                if (element is ParametricCellElement instance)
                {
                    MessageCenter.Instance.StatusMessage = $"Parametric cell: {instance.CellDefinition.CellName}";
    
                    List<Element> extracted = new List<Element>();
    
                    try
                    {
                        ParametricCellProcessor processor = new ParametricCellProcessor(extracted, model);
                        ElementGraphicsOutput.Process(instance, processor);
    
                        foreach (Element el in extracted)
                        {
                            el.AddToModel();
                        }
                    }
                    finally
                    {
                        if (null != extracted)
                        {
                            foreach (Element el in extracted)
                            {
                                el.Dispose();
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                return new NodeUpdateResult.TechniqueException(ex);
            }
    
            return NodeUpdateResult.Success;
            
        }
    } // class
    
    internal class ParametricCellProcessor : ElementGraphicsProcessor
    {
        private readonly IList<Element> elements;
        private readonly DgnModel dgnModel;
        private DTransform3d trans;
    
        private ParametricCellProcessor()
        {
        }
    
        public ParametricCellProcessor(IList<Element> elements, DgnModel dgnModel)
        {
            this.elements = elements ?? throw new ArgumentNullException(nameof(elements));
            this.dgnModel = dgnModel ?? throw new ArgumentNullException(nameof(dgnModel));
        }
    
        public override void AnnounceTransform(DTransform3d trans)
        {
            this.trans = trans;
        }
    
        public override bool ProcessAsBody(bool isCurved) => false;
    
        public override bool ProcessAsFacets(bool isPolyface) => false;
    
        public override BentleyStatus ProcessCurveVector(CurveVector curves, bool isFilled) => BentleyStatus.Error;
    
        public override BentleyStatus ProcessCurvePrimitive(CurvePrimitive curve, bool isClosed, bool isFilled)
        {
            Element element = DraftingElementSchema.ToElement(this.dgnModel, curve, null);
            element.ApplyTransform(new TransformInfo(this.trans));
            this.elements.Add(element);
            return BentleyStatus.Success;
        }
    }

    Regards,

      Jan

    Answer Verified By: Edward Ashbolt 

  • Seriously? With all respect, it sounds to me it's easier for you to ask then to search anywhere:

    • I see 3 examples where DraftingElementSchema is used i MicroStation SDK examples
    • I received 67 results searching for DraftinElementSchema on this web

    Isn't it enough?

    I found 9 references to the DraftingElementSchema on this forum (it seems I should have expanded my search to all communities), and a few references to it in the API, which did help me but not to the extent that I fully understood whether my approach is correct and what the function of the template element is. Believe me, I wish it were enough... but unfortunately I am still learning, slowly and painfully trying to piece together the full picture, but still getting errors along the way so any information that people like yourself/Brien/Jon share is very helpful and very much appreciated!

    I'm certainly not just trying to take the easy road, if I was I wouldn't have even bothered trying to create this functionality in the first place and just used Dynamo instead.

    I guess this rule has existed from V8.0 APIs (VBA, C, Interop...): When there is no element that should be used as a template available, use null

    Interesting, I did set it to null and it wasn't happy and threw an error, I guess I have some more issues to resolve then ;)

    use assembly decompiler to learn how NET wrapper is implemented

    That's a great idea, I will look into this... Are there any in particular that you would recommend? I might give the JetBrains decompiler a go since that is open source.

    New to what?

    New to the Microstation API/SDK, and GC node creation. Most of the libraries and functions are new to me and this is my first attempt at creating a tool like this on the platform; it seems that I may have bitten off more than I can chew... I do enjoy a good challenge though and the silver lining is I will learn something new and have that knowledge for next time round. I am not a developer by trade and have had no formal training, I just enjoy creating scripts and extending software to gain efficiencies at work. If you know of any courses / workshops / blogs etc. that might help accelerate my learning curve for programming with Microstation API, that would be great if you could share your experience & knowledge on this matter!

    weak knowledge of basic OOP concepts (SOLID etc.)

    First time I've heard of that concept, thanks for sharing that with me. It makes a lot of sense and certainly makes code a lot easier to manage if written with those principles in mind

    In my opinion dependency injection is the solution here

    Yep that is definitely the end goal to make it more flexible; I am just attempting to keep everything as simple and bare bones for now so that I can prove the fundamental concept and troubleshoot problems more easily.

    When you say "error", it's like silence. What error? Is an exception thrown? When yes, what exception?

    "External Component has thrown an exception" and then proceeds to kill the application. Obviously I have some serious flaws in my approach; I thought it was due to the null value but it seems I have other issues to resolve.

    What fashion (because I do not know about any reason) I have seen in many shared snippets to pollute code with full classes names (including namespaces)?

    Yes I know, it does look ugly... It is due to ambiguous references between libraries; I should create an alias to tidy things up but my sole focus has been on solving the fundamental solution, I started with a sloppy approach during testing and haven't gotten round to fixing it... I'll certainly tidy things up and make the code much more structured once I've got the fundamentals in order!

    I do not know GC API, so I am not sure what is the best way, but to use ModelRef to obtain element plus using explicit casting (which potentially can ends with casting exception) looks not very elegant

    That was the only method I could come up with at that stage, I see you have fixed that with your code sample; that is a much neater and safer approach indeed!

    This should work (at least MicroStation part), but be aware it's a concept, the structure is not fine (especially how elements are disposed):

    Thanks again for all your help! I will take a look at the code and let you know how it all goes... Really appreciate all the advice and assistance, I definitely wouldn't have gotten this far otherwise!
    Cheers,

    Ed

  • Jan you are legend, it works great! Thanks so much for all your advice and assistance with this, I definitely wouldn't have gotten this far otherwise. Really stoked to finally get that operational! If you ever make it over to this part of the world (Melbourne, Australia) I will be buying you all the beers as thanks ;)
    Cheers,

    Ed