Site Meter
 
 

Editing a variable-length list of items in ASP.NET MVC

Update: This post was originally written for ASP.NET MVC 1.0 Beta. I’ve written a newer version of this post that applies to ASP.NET MVC 2.0 (Release Candidate). Note that this technique doesn’t actually work quite so easily with the final version of ASP.NET MVC 1.0, because its model binding convention changed in a way that means you have to massage the data into a numerical sequence for it to work. See comment #25 for a hint about one way to do this, or upgrade to ASP.NET MVC 2.0.

Most web applications involve editing lists or collections of things at some point. For example,

  • Each blog post has a collection of tags
  • Each flight booking has a list of passenger names and corresponding food preferences
  • Each recipe has a collection of ingredients with their quantities and prices

It’s always awkward to create the right UI for variable-length lists, because you don’t know how many input controls to render. The user needs a way to add and remove items, and you have to retain all this state if they submit a form that causes validation errors.

How would you do it with ASP.NET MVC? There are loads of possible ways of doing it! In this post, I’ll show you one fairly elegant way I settled on in a recent project.

Try it yourself (launch live demo) Download the source code

Getting started

I’ll assume you’re already building an application with ASP.NET MVC (Beta). Since it’s that time of year, let’s build a gift list editor. We can model a gift as follows:

public class Gift
{
    public int GiftID { get; set; }   // Unique key
    public string Name { get; set; }
    public double? Price { get; set; }
}

Rendering the initial UI

To render the initial UI, add an action method similar to the following:

[AcceptVerbs(HttpVerbs.Get)]
public ViewResult EditList()
{
    var initialData = new[] {
        new Gift { GiftID = 14, Name = "iPod Nano 16GB Red", Price = 184.95 },
        new Gift { GiftID = 221, Name = "Noddy: The Deleted Scenes", Price = 19.99 }
    };
 
    return View(initialData);
}

This sets up an initial gift list and renders a view based on it. In a real app, you might fetch this initial data from your model tier or database.

Next, you’ll need a view template for the EditList() action. Right-click inside the EditList() method and choose Add View. We’re going to render a series of Gift objects, so make the view Strongly Typed, using a model type of IEnumerable<Gift>. Here’s our view template:

<h1>Gift request form</h1>
 
<% using(Html.BeginForm()) { %>
 
    <div id="items">
        <% foreach (var gift in ViewData.Model) {
            Html.RenderPartial("GiftEditor", gift, new ViewDataDictionary(ViewData) {
                {"prefix", "gifts"}
            });
        } %>
    </div>
 
    <input type="submit" value="Save changes" />
<% } %>

As you can see, this iterates over the incoming collection of Gift objects, and for each one, it renders a partial view called GiftEditor. The GiftEditor partial will hold the UI needed to edit each entry in the collection.

To create the Gift Editor partial, right-click on the /Views/Shared folder, and choose Add –> New Item. Make an MVC View User Control called GiftEditor.ascx, then go to its code-behind class and set it to inherit from ViewUserControl<Gift>, i.e.,

public partial class GiftEditor : ViewUserControl<gift>
{
}

Now you can go back to GiftEditor.ascx and add the markup representing the UI controls for a single Gift object:

<div>
    <input type="hidden" name="<%= ViewData["prefix"] + ".index" %>" value="<%= ViewData.Model.GiftID %>" />
    <% var fieldPrefix = string.Format("{0}[{1}].", ViewData["prefix"], ViewData.Model.GiftID); %>
 
    <%= Html.Hidden(fieldPrefix + "GiftID", ViewData.Model.GiftID) %>
    Gift name: <%= Html.TextBox(fieldPrefix + "Name", ViewData.Model.Name, new { size = "30"})%>
    Price ($): <%= Html.TextBox(fieldPrefix + "Price", ViewData.Model.Price, new { size = "5" })%>
</div>

If you don’t have a clue what’s going on here, don’t worry! This isn’t obvious. I’m following ASP.NET MVC’s control naming convention, putting clusters of input controls into groups using a kind of array notation. For more details about how this works, see Phil Haack’s post on the subject. The benefit of following this convention will become clear in a moment when we try to post the data back to the server.

With all this, the initial UI will now appear!

image

Receiving posted data

OK, so the user can edit the existing records and can click “Save Changes”. But when they do, they’ll get a 404 Not Found error because you don’t have any action method to receive the post. Add an action method as follows:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult EditList(IList<gift> gifts)
{
    return ModelState.IsValid ? View("Completed", gifts)
                              : View(gifts);
}

This is the magic of model binding: because we followed its naming convention earlier, we can now receive a fully-populated collection of .NET objects as an action method parameter. All the incoming data will be parsed into the collection automatically.

All the new action has to do is decide whether or not the incoming data is good.

  • If the data is good, the action will render the “Completed” view. That’s nothing special; it’s just any old view.
  • If the data is bad – for example because you typed some nondigit characters in a “Price” box – the action will re-render the same EditList view you created earlier.

Adding new items

So far, so good. But the user can only edit existing items… what about adding new ones? Well, that’s easy: we can use a bit of Ajax to inject extra edit rows on the fly.

Add the following code into EditList.aspx, just above the “Submit Changes” button:

<%= Ajax.ActionLink("Add another item", "BlankEditor", new AjaxOptions {
    UpdateTargetId = "items", InsertionMode = InsertionMode.InsertAfter
}) %>

Here, we’re using ASP.NET MVC’s built in Ajax helper to fetch the output of an action method and inject it directly into the existing DOM (note that your master page needs to reference ~/Scripts/MicrosoftAjax.js and ~/Scripts/MicrosoftMvcAjax.js for this to work). Specifically, we’re putting the output of an action called BlankEditor at the bottom of the existing DOM element called items.

Obviously, you also need to add an action method called BlankEditor. It simply returns a blank edit row:

public ActionResult BlankEditor()
{
    // Use convention that negative IDs represent unsaved records
    int tempUniqueID = -1 * new Random().Next();
 
    ViewData["prefix"] = "gifts";
    return View("GiftEditor", new Gift { GiftID = tempUniqueID });
}

That does it! Now there’s an Add another item link which causes a new edit row to appear. You can fill in some data, and model binding will automatically parse it as a Gift object. If there are validation failures, the number of input controls (and their contents) will all be preserved because of how we’re following the model binding conventions.

Deleting items

Deleting an item is simply a matter of removing the corresponding DOM elements. When the form is submitted, that data will no longer be sent, so there will be no corresponding item in the model-bound collection. Add the following link to the end of GiftEditor.ascx:

<a href="#" onclick="deleteContainer(event)">Delete</a>

(For example, put this just after the Price textbox.) To make it do something, add the following JavaScript function to some JavaScript file referenced by your page:

function deleteContainer(evt) {
    evt = evt || window.event;
    var target = evt.target || evt.srcElement;
    target.parentNode.parentNode.removeChild(target.parentNode);
}

(For example, put it inside a <script> block somewhere in EditList.aspx) Of course, I could have done this using jQuery, and then I wouldn’t have had to deal with window.event, evt.srcElement and the other cross-browser nastiness. However, I’ve chosen not to use jQuery in this example just to avoid forcing readers to understand a further technology.

And that deals with deletion! You now have a fully-functional list editor that can add and remove items, and can preserve state if you need to deal with validation failures.

image

Remarks

In most applications, you’ll also need to do something with the incoming data, such as saving it to your database. I’m not going to cover that here because there are so many different data access technologies and you can use whatever you normally use.

For example, if you’re using LINQ to SQL, you’ll probably need to edit your POST handler (the EditList() action that responds to POST requests) so that it loads the existing collection from disk, deletes any records not present in the new data, updates any records still present in the new data (e.g., by using UpdateModel() to automatically transfer changes to the LINQ to SQL entity), and adds any new records (they’re the ones with the temporary negative IDs).

There’s also the matter of enforcing arbitrary validation rules. There’s nothing clever about this; you can do it however you normally do validation. In the downloadable demo project, you’ll see how I usually do it. Specifically, I keep the rules in my model tier, then let the model tier enforce compliance by throwing an exception if the rules are violated. I set this up so that the violation messages are automatically displayed at the right place in the UI. For example, in the live demo, try entering a negative price. Download the demo code to see how it works.

Is there a better way?

If you’ve implemented a variable-length list editor with ASP.NET MVC, how was the experience for you? If you think you’ve got a better way of doing it, I’d really like to hear about it – please post a brief comment explaining what you do! (But please don’t post massive code samples because I’d have to edit it down – try to explain your technique succinctly.)

In case anyone’s interested, it’s really easy to give the user the ability to drag-and-drop the items into a different order, and then to communicate that order to the server. I might well show that in a follow-up post.

kick it on DotNetKicks.com

42 Responses to Editing a variable-length list of items in ASP.NET MVC

  1. I use jqModal (jQuery plugin – http://dev.iceburg.net/jquery/jqModal/ ) that uses the href feature and loads a form for editing in a dialog – I’m not a big ‘datagrid editing fan’.

    It uses a trigger to make an ajax call to load a form with the form data.

    a class=editTrigger href=Product/Edit/{id}

    so I built a html helper to do the dialog

    html.dialog(a.edittrigger)

    it injects the dialog javascript in the page, wires up the dialog to the trigger

    I like your sample – very nice ! My example makes a call to save each item with ajax, whereas your doing more of a bulk save – so I like that.

  2. Hi Steve,
    Yes, there are many ways to do this, for example we can use jqgrid plugin for jquery. Or use one form for one element in the collection with jquery jforms plugin to ajaxify the page. In this case we need to add save link or button to each item. In order to save all list at once we’ll need to write some javascript to serialize each form and post it to the server.
    Voices in my head suggest many other ways but I’m lazy to write them all ;) . I described the shortest ones.
    Thank you.

  3. Steve

    @Steve Gentile, @Vasili – Those approaches sound good for editing individual items, but what about adding and removing items? Can you do multiple additions/deletions/edits within a single transaction, or do you have to commit each change to disk separately and immediately when it’s made?

  4. Pingback: Reflective Perspective - Chris Alcock » The Morning Brew #250

  5. Nice approach and elegant (except the inline script declaration and non-semantic anchor href…).

    One of the main reasons I made the MVC XForms framework was for this requirement, editing of arbitrary lists. You can have a look at http://www.codeplex.com/mvcxforms. I explain the repeat functionality at http://www.joncurtis.co.uk/post/MVC-XForm-container-controls-XForm-Group-and-Repeat.aspx.

    Basically, you would specify your repeat like so:

    <%= form.Trigger(”AddGift”)
        .Label(”Add Gift”)
        .Insert(i => i.Nodeset(m => m.Gifts).At(At.Last)) %>

    <% form.Repeat(m => m.Gifts).ItemTemplate(gifts => { %>
        <%= gifts.Input(l => l.GiftID) %>
        <%= gifts.Input(l => l.Name) %>
        <%= gifts.Input(l => l.Price) %>
        <%= gifts.Trigger(”Del”).Label(”x”).Delete() %>
    <% }); %>

    Behind the scenes, MVC XForms uses a response filter to capture the output of the lambda. It then creates a template in javascript for this repeater. Whenever the insert action is fired by the trigger, the script inserts a new item (with updated list prefixes). So we get client side insertion and deletion of arbitrarily complex lists – even nested ones.

    As you can see, there is very little code required to achieve this functionality, and it is all clean html and separated javascript.

  6. Steve

    Jon, your MVC XForms project is extremely interesting and I’ll certainly be keeping an eye on it. Can’t wait until browsers support XForms natively.

    Sorry about the difficulty you had with the tags being stripped out of your comment. WordPress has such a lame approach to sanitizing input – permanently storing a mangled version in the database instead of simply escaping it at the point of display. It’s so annoying I might well switch to a different blog platform when I get time.

    > except the inline script declaration and non-semantic anchor href

    Yeah, I know…! But until browsers do support XForms natively, there’s no way of giving the “delete” or “add” buttons any true semantic meaning that a machine would understand, and it’s not like I’m suggesting this code supports non-JavaScript clients.

  7. Not a bad way to skin this cat. Probably the most awkward part is constructing the HTML element names to use array notation so that the binder will handle it properly.

    MvcContrib.FluentHtml can help with this thorny part. Instead of all the fancy string formatting in the view, you could instead have something like this:

    x.Gift[i].GiftId)%>
    x.Gift[i].Name).Label(“Name:”).Size(30)%>
    x.Gift[i].Price).Label(“Gift:”).Size(5)%>

    This will name the HTML element so they are understood by most binders. I blog about it here: http://lunaverse.wordpress.com/2008/11/24/mvcfluenthtml-fluent-html-interface-for-ms-mvc/

  8. The code in my previous post got mangled ’cause I didn’t HtmlEncode it. Let me try again:

    <%=this.Hidden(x => x.Gift[i].GiftId)%>
    <%=this.TextBox(x => x.Gift[i].Name).Label(“Name:”).Size(30)%>
    <%=this.TextBox(x => x.Gift[i].Price).Label(“Gift:”).Size(5)%>

  9. Steve

    @Tim, that’s cool. I certainly agree that the built-in HTML helpers could do a better job of constructing the index names for us. For example, we could pass a “prefix” and an “index” parameter as well as “name”, then it would render “prefix[index].name”. Unfortunately this would require adding loads more overloads to the HTML helper methods. Maybe still worth it though.

    Your fluent helpers are pretty neat – in effect, they give you optional parameters and bypass the combinatoric nightmare. I think you would need to add extra .Prefix() and .Index() methods to generate the element IDs properly – the example code you posted above won’t really work because in this post’s example we need to index by PersonID value and not just by position (the variable “i” in your code) in the data set (otherwise, as elements are added and removed, you lose track of which change applies to which object).

  10. Pingback: Dew Drop - December 23, 2008 | Alvin Ashcraft's Morning Dew

  11. @Steve

    We don’t need Prefix() and Index() because we use the provided expression to generate the name. Let’s say your view implements ModelViewUserControl<Foo> (ModelViewUserControl<T> is a FluentHtml class that derives from ViewUserControl<T>). Let say Foo has a property IList<Bar> Bars. Then this:

    <%=this.Hidden(x => x.Bar[i].BarId)%>

    …will generate HTML like this:

    <input type=”hidden” name=”Bar[0].BarId” id=”Bar[0]_BarId” value=”123″/>

    And this:

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Save([Bind]Foo foo)

    …will populate the foo.Bars collection correctly from the view.

    Here’s the class that turns the expression into the name: http://code.google.com/p/mvccontrib/source/browse/trunk/src/MvcContrib.FluentHtml/Expressions/ExpressionNameVisitor.cs

  12. Steve

    @Tim – thanks for responding. I do understand what you’re saying, and it’s really cool that you’ve made the fluent HTML helpers which can do that. They have a lot of potential to make view templates neater.

    What I was trying to explain is that your strategy doesn’t quite work in this particular example. There are two show-stopping problems:

    [A] If each entry is rendered using a partial view, passing that entry to the partial as ViewData.Model, then the partial doesn’t know the index of its model object in the outer collection. The partial doesn’t even know that the outer collection exists. So where would you get the variable “i” that you show in your code snippets?

    You can extrapolate this problem to nested hierarchies of partials. None of them would know where they fit in the overall hierarchy unless you explicitly pass them some kind of name prefix.

    [B] If the user can add and remove items on the client (as in the gift list example), then your indexes can get out of sync with your entities. Consider this scenario:

    1. Your initial data set consists of A, B, C. Using your code, these become controls named gifts[0].Prop, gifts[1].Prop, and gifts[2].Prop.
    2. The user deletes entity A, and enters some unparseable data into entity B, then submits the form.
    3. Model binding will try to parse the incoming gifts[1].Prop value (for entity B), get an error, and registers the error in ModelState using the key “gifts[1].Prop”. This is ASP.NET MVC’s default model binding convention.
    4. You re-render the form. This time, there are only items B and C, so your code names the controls gifts[0].Prop (for B) and gifts[1].Prop (for C).
    5. The binding error message appears next to gifts[1].Prop, because that’s the key it was registered under. So entity B’s error message now appears next to the controls for entity C. Whoops!

    This is why positional indexing isn’t sufficient. You need indexes tied to the identity of the entities being rendered – not their positions in a collection.

    If you altered your fluent helpers to let the user specify a prefix and an index (e.g. TextBox().Prefix(someString).Index(x => x.GiftID)) then you could solve both problems [A] and [B].

  13. Pingback: Drag-and-drop sorting for our ASP.NET MVC list editor (in one line of code) « Steve Sanderson’s blog

  14. Pingback: Editing a Variable Length List Of Items With MvcContrib.FluentHtml « Lunablog

  15. @Steve

    Thanks for the detailed response to my rather weakly developed suggestion. I decided to force myself to think this through by writing my own blog post about it:

    http://lunaverse.wordpress.com/2009/01/02/editing-a-variable-length-list-of-items-with-mvccontribfluenthtml/

  16. I just posted my take on doing variable list binding here:

    http://justsimplecode.com/archive/2009/01/07/my-take-on-variable-length-lists-in-asp.net-mvc.aspx

    please let me know what you think, thanks.

  17. Pingback: xVal - a validation framework for ASP.NET MVC « Steve Sanderson’s blog

  18. Pingback: Editing a Variable Length List Of Items With MvcContrib.FluentHtml - Take 2 « Lunablog

  19. Vivek

    Steve, Thanks for the great post.

    I need to render my list in a table and add a new item as a row to that table. I have not been able to add a row using UpdateTargetId. Any ideas how this can be accomplished?

    Thanks

  20. Steve

    @Vivek – you might be able to add TR elements to a TBODY if you give an ID to the TBODY.

    If that doesn’t work, then you might want to get rid of the Ajax.ActionLink and use jQuery to fetch and insert the new element instead. For an example of how to use jQuery to do this, see Tim Scott’s post at http://lunaverse.wordpress.com/2009/01/02/editing-a-variable-length-list-of-items-with-mvccontribfluenthtml/ (look under the “Add Behavior” heading).

  21. Damien

    For me
    did not populate the IList gifts variable. I had to use
    <form method=”post” action=”"> for it to get populated

  22. Steve

    @Damien – are you using ASP.NET MVC Beta, or some other version? Or might you have something different about your routing config?

  23. Sarp

    Steve is this example work on MVC RC?
    I think we cannot get the list of form posted values as a list parameter like this way.

  24. Steve

    @Sarp – the RC has changed the way model binding indexing works. It now requires you to supply all indexes in an unbroken ascending sequence, i.e., gifts[0].propName, gifts[1].propName, gifts[2].propName, etc. This makes things more difficult when you’re changing the collection dynamically on the client.

    One possible workaround is to add a JavaScript function that, when the form is being submitted, changes the "name" attributes on all your controls to ensure they’re in an unbroken ascending sequence, regardless of how they’ve been edited. For example, add the following:

    $(function() {
        $("form").submit(function() {
            // Normalize the sequence before submitting the form
            ensureControlGroupsInNumericalOrder("#items div", "gifts");
        });
    });

    function ensureControlGroupsInNumericalOrder(groupSelector, controlNamePrefix) {
        var groups = $(groupSelector);
        var nameToEndOfIndexRegex = new RegExp(controlNamePrefix + "\\[\\d+");
        for (var seq = 0; seq < groups.length; seq++) {
            var controls = $("*[name~=" + controlNamePrefix + "\[]", groups[seq]);
            for (var c = 0; c < controls.length; c++)
                controls[c].name = controls[c].name.replace(nameToEndOfIndexRegex, controlNamePrefix + "[" + seq);
        }                
    }

    You also need to change my example code so that when it initially renders the input controls, it indexes them by sequence and not by GiftID. With all this done, it works in ASP.NET MVC RC. Some of the rest of my example code is now unnecessary (e.g., emitting gifts.index values).

    I know that’s not as simple and clear as you’d hope. I might at some point update this post or do another post to show the complete updated code.

  25. Rob

    Steve, when using that javascript to mod the names I get “exception thrown but not caught” error clientside.. any ideas? I think it could perhaps be corruption from cut paste from the blog, got a file link?

  26. Steve

    @Rob – it might have been because WordPress converted all my regular straight double quotes to curly ones. I’ve edited comment #25 so that the quotes come out straight again. It will probably work now!

    Also I should have mentioned originally, in case it isn’t obvious, that this workaround requires you to have jQuery referenced.

  27. Rob

    I had fixed up the quotes and included JQuery 1.3.1, I’m starting to think this is a bug in jQuery (there seem to be more reports of this kind of problem with 1.3.1).

  28. Rich Urwin

    Steve,

    I see you’re using AJAX here so thought it the best place to ask this question:

    What do you do when the AJAX request is unauthorised (for example if the forms auth has timed out.)

    At best, nothing happens, and at worst, the login screen is displayed where the results of the AJAX should appear, depending on the redirection.)

    Is the only way to cope with this a flimsy ‘check ajax response and do javascript redirect’?

    Cheers,
    Rich

  29. Steve

    @Rich – I’m not aware of a great way of dealing with that situation. As you suggest, one approach is to embed some special token in your login view, then have the client-side code notice this token and behave differently when it sees it.

    A further alternative is to write your own custom authorization filter, and make it able to notice when the request is an Ajax request (using request.IsAjaxRequest). If so, it could return a 403 status (instead of a 302 redirection to the login page) so that the client-side code would know unequivocally that authorization had failed.

  30. Rich Urwin

    @Steve,
    I like your custom auth filter idea – think I’ll go with this. Much neater.

    Thanks for the help as ever,
    Rich

  31. Mike Roosa

    How come if you start typing a value in the gift field then hit the Add another item button, it wipes out what you just typed?

  32. rickm

    @Mike: This is a known MVC bug. See http://www.codeplex.com/aspnet/WorkItem/View.aspx?WorkItemId=2893

    This change to MicrosoftMvcAjax.debug.js is working for me:

    Sys.Mvc.MvcHelpers.updateDomElement = function Sys_Mvc_MvcHelpers$updateDomElement(target, insertionMode, content) {
    if (target) {
    switch (insertionMode) {
    case Sys.Mvc.InsertionMode.replace:
    target.innerHTML = content;
    break;
    case Sys.Mvc.InsertionMode.insertBefore:
    if (content && content.length > 0) {
    var tempNode = document.createElement(“DIV”);
    tempNode.innerHTML = content;
    target.insertBefore(tempNode);
    }
    break;
    case Sys.Mvc.InsertionMode.insertAfter:
    if (content && content.length > 0) {
    var tempNode = document.createElement(“DIV”);
    tempNode.innerHTML = content;
    target.appendChild(tempNode);
    }
    break;
    }
    }
    }

  33. Darren

    Steve,

    Great post – exactly what I’m looking for.

    Similar to Rob’s post I can’t get ensureControlGroupsInNumericalOrder working in ASP.NET MVC R1. I get an exception thrown but not caught followed by an object doesn’t support this property or method.

    Exception occurs at the line

    var controls = $(“*[name~=" + controlNamePrexif + "\[]“, groups[seq]);

    Any ideas how to remedy this, groups[seq] appears to be valid so I’m guessing its something to do with the regex?

    Thanks

  34. Anthony

    Has anyone had chance to take this as far as saving the list to a database. Im having issues finding a clean way to save a list.

    My issue is I cannot save a list using UpdateModel() and have found no resources on how to do it.

    I can ofc save my list but use for loops to update the data line by line.

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult EditList(IList gifts)
    {
    IList dbGifts = myRepository.GetGifts().ToList();

    UpdateModel(dbGifts, “gifts”);

    myRepository.Save();

    return View(gifts);
    }

    The above I would expect to work, and the data ‘dbGifts’ does get updated, but on .Save() I get ‘Cannot add an entity with a key that is already in use.’ when a try catch is implemented.

    I’m not after a whole solution but a pointer would be fantastic. I have not found a clean solution using UpdateModel() of which I’m sure is possible.

    Thanks

  35. Griti

    Is there any update in sight?
    I modified all and it kind of works but in IE I get an annoying error because the event is null.

    Anyway to big to post here, just asking for the state.

    Or has any another solution that works for partial forms?

  36. In this process, the media is crucial; blogs, and mass media. ,

  37. Thanks for the article.
    But I seem to get null for list of gift in
    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult EditList(IList gifts)

    Can you point out why?

  38. Craig Howard

    Save isn’t working for me. MVC 2 Release Version.

    When posting back to EditList, gifts is null

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult EditList(IList gifts)
    {
    //gifts is null, so I get an exception

    }

  39. Admiring the commitment you put into your website and detailed information you present. It’s good to come across a blog every once in a while that isn’t the same old rehashed material. Wonderful read! I’ve bookmarked your site and I’m including your RSS feeds to my Google account.

  40. It’s a means of getting new clients and increasing the visibility of a company’s products and services among the target market. Some trade shows are open to the public, while others can only be attended by company representatives (members of the trade) and members of the press.

  41. Es scheint so, dass dein Artikel fehlerhaft strukturiert ist. Liegt dies, an meinem Browser?