Developer forum

Forum » PIM » Feed: data to CSV

Feed: data to CSV

Alex Guo
Reply

Hey,

I'm an intern and new with dynamic web.

I am struggling to transform the product data to a csv. 
The code I currently have is displaying the id and name, but not the variantId, or Numbe, these show as empty:

@using Dynamicweb.Rendering;
@using Dynamicweb.Ecommerce.ProductCatalog;
@using System.Collections.Generic;
@using System.Text;
@inherits ViewModelTemplate<ProductListViewModel>
@functions
{
    public enum FieldType
    {
        System,
        Standard
    };
 
    public class Field
    {
        public string ID { get; set; }
        public FieldType FieldType { get; set; }
        public string Label { get; set; }
 
        public Field(string id, string label, FieldType type)
        {
            ID = id;
            Label = label;
            FieldType = type;
        }
    }
 
    public List<Field> GetFields(ProductViewModel productViewModel)
    {
        var fields = new List<Field>();
        if (productViewModel == null)
        {
            return fields;
        }
 
        fields.Add(new Field("Id", "Product Id", FieldType.System));
        fields.Add(new Field("VariantId", "Variant id", FieldType.System));
        fields.Add(new Field("Number", "Number", FieldType.Standard));
        fields.Add(new Field("Name", "Name", FieldType.Standard));
       
        return fields;
    }
 
    public string GenerateCsv()
    {
        var fields = GetFields(Model.Products.FirstOrDefault());
 
        var allHeaders = fields.Select(x => new KeyValuePair<string, string>(x.ID, x.Label)).ToList();
 
        var headerSystemNames = allHeaders.Select(x => x.Key).ToList();
 
        StringBuilder contentBuilder = new System.Text.StringBuilder();
 
        contentBuilder.Append(Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble()));
 
        var headerLine = string.Join(";", allHeaders.Select(x => String.Format("\"{0}\"", x.Value))).ToString();
 
        contentBuilder.AppendLine(headerLine);
 
        var productsOuput = new StringBuilder();
        foreach (var product in Model.Products)
        {
            var values = new string[headerSystemNames.Count];
           
            values[headerSystemNames.IndexOf("Id")] = product.Id;
            values[headerSystemNames.IndexOf("VariantId")] = product.VariantId;
            values[headerSystemNames.IndexOf("DefaultVariantId")] = product.DefaultVariantId;
            values[headerSystemNames.IndexOf("Number")] = product.Number;
            values[headerSystemNames.IndexOf("Name")] = product.Name;
 
            productsOuput.AppendLine(string.Join(";", values.Select(x => String.Format("\"{0}\"", (x == null ? "" : x.Replace("\"", "'"))))));
        }
 
        contentBuilder.Append(productsOuput.ToString());
 
        return contentBuilder.ToString();
    }
}
@GenerateCsv()

This is the result: 

The variant Id and number are empty. I dont need the defaultVariantId that was just for testing.
 

 

Thanks in advance!

 

 


Replies

 
Nicolai Pedersen Dynamicweb Employee
Nicolai Pedersen
Reply
This post has been marked as an answer

The problem is that you’re trying to fill in a field ("DefaultVariantId") that isn’t even part of your header list. In your foreach loop you have:

values[headerSystemNames.IndexOf("DefaultVariantId")] = product.DefaultVariantId;

Since "DefaultVariantId" isn’t added in your GetFields method, calling IndexOf("DefaultVariantId") returns –1. This can cause unexpected behavior (or an exception) that interferes with filling in the correct values for the VariantId and Number fields.

Simplified CSV Code

A simpler approach is to hard-code the header and iterate over your products without doing the header lookup via indices. For example:

@functions {
    public string GenerateCsv()
    {
        var sb = new System.Text.StringBuilder();

        // Write CSV header
        sb.AppendLine("\"Product Id\";\"Variant Id\";\"Number\";\"Name\"");

        // Write each product's data
        foreach (var product in Model.Products)
        {
            sb.AppendLine(
                $"\"{(product.Id ?? "")}\";" +
                $"\"{(product.VariantId ?? "")}\";" +
                $"\"{(product.Number ?? "")}\";" +
                $"\"{(product.Name ?? "")}\""
            );
        }

        return sb.ToString();
    }
}

This code does the following:

  • Writes a fixed header line with the correct field labels.
  • Loops over each product and outputs the corresponding fields using string interpolation.
  • Handles potential null values by replacing them with an empty string.

This approach is not only more straightforward but also avoids the error of mismatched header keys.

Votes for this answer: 1
 
Alex Guo
Reply

Yes, I noticed a little later after posting that I forgot to remove `DefaultVariantId` from the headers.
However, the initial issue still persists—`VariantId` and `Number` are both empty in the generated CSV.  

I found the issue, Thanks for the fast reply

 
Alex Guo
Reply

How can I get the language for the feed
It has to be the language name not the ID

And how do i display the GroupPaths

 
Nicolai Pedersen Dynamicweb Employee
Nicolai Pedersen
Reply

You can get the language from using the language service: Dynamicweb.Ecommerce.Services.Languages - and then the product has a languageid that can be used to lookup the language and you can take the name from there.

You can create a string from the group path property something like this:

string groupPath = string.Join(",", GroupPaths.SelectMany(gr => gr).Select(item => item.Name));

 
Alex Guo
Reply
I keep getting this error when adding the language service.

- error: (44, 10) A 'using namespace' directive can only be applied to namespaces; 'Services' is a type not a namespace. Consider a 'using static' directive instead
 - error: (50, 40) The type name 'Languages' does not exist in the type 'Services'
 
Imar Spaanjaars Dynamicweb Employee
Imar Spaanjaars
Reply

Can you show your code? Otherwise it's a bit difficult to help :-)

 
Alex Guo
Reply
@using Dynamicweb.Rendering;
@using Dynamicweb.Ecommerce.ProductCatalog;
@using System.Collections.Generic;
@using System.Text;
@using System.Linq;
@using Dynamicweb.Ecommerce;
@using Dynamicweb.Ecommerce.Services;
@inherits ViewModelTemplate<ProductListViewModel>
 
@{
    string languageId = Dynamicweb.Core.Converter.ToString(
        Dynamicweb.Context.Current.Request.QueryString["languageId"]) ?? "LANG1";
}
 
@functions
{
    public class Field
    {
        public string ID { get; set; }
        public string Label { get; set; }
 
        public Field(string id, string label)
        {
            ID = id;
            Label = label;
        }
    }
 
    public List<Field> GetFields()
    {
        return new List<Field>
        {
            new Field("Id", "Product Id"),
            new Field("VariantId", "Variant Id"),
            new Field("Number", "Number"),
            new Field("Name", "Name"),
            new Field("GroupPaths", "GroupPaths"),
            new Field("LanguageId", "LanguageId"),
            new Field("VarientName", "VarientName"),
            new Field("ProductGroup", "ProductGroup"),
        };
    }
 
    public string GenerateCsv(string currentLanguageId)
    {
        var fields = GetFields();
        var headerSystemNames = fields.Select(f => f.ID).ToList();
 
        var contentBuilder = new StringBuilder();
        contentBuilder.Append(Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble()));
 
        // Generate CSV header
        contentBuilder.AppendLine(string.Join(";", fields.Select(f => $"\"{f.Label}\"")));
 
        // Generate CSV rows
        foreach (var product in Model.Products)
        {
            var values = new string[headerSystemNames.Count];
            for (int i = 0; i < fields.Count; i++)
            {
                values[i] = GetFieldValue(product, fields[i].ID, currentLanguageId);
            }
 
            contentBuilder.AppendLine(string.Join(";", values.Select(v => "\"" + v.Replace("\"", "'") + "\"")));
        }
 
        return contentBuilder.ToString();
    }
 
    public string GetFieldValue(ProductViewModel product, string fieldId, string currentLanguageId)
    {
        switch (fieldId)
        {
            case "Id":
                return product.Id ?? "";
            case "VariantId":
                return product.VariantId ?? "";
            case "Number":
                return product.Number ?? "";
             case "Name":
                return product.Name ?? "";
                //test
            case "GroupPaths":
                try {
                    if (product.GroupPaths != null) {
                        string groupPath = string.Join("|", product.GroupPaths.SelectMany(gr => gr).Select(item => item.Name));
                        return groupPath;
                    }
                } catch {}
                return "";
            case "LanguageId":
                return currentLanguageId;
            case "VarientName":
                return product.VariantName ?? "";
                //test
            case "ProductGroup":
               return Dynamicweb.Ecommerce.Services.Groups.GetGroup("Group1").Name;
                return "";              
            default:
                return "";
        }
    }
}
 
@GenerateCsv(languageId)
 
Imar Spaanjaars Dynamicweb Employee
Imar Spaanjaars
Reply

There are a couple of issues with your code. First, there's this:

@using Dynamicweb.Ecommerce.Services;

Ecommerce.Services is a type, not a namespace so it cannot be used in a using statement. You can just remove this line.

Then there's this:

return Dynamicweb.Ecommerce.Services.Groups.GetGroup("Group1").Name;

 

On my version of DW, Services.Groups should be Services.ProductGroups. Not sure if that's different on your version of DW though.

Finally, your error message also mentioned Languages but I don't see that in your code. The Languages service lives here: Dynamicweb.Ecommerce.Services.Languages

Hope this helps,

Imar

BTW: when posting code, can you format it a bit for readability? With all the line breaks it's harder to read than necessary. You can use the Formatted style on the toolbar for a fixed-width font:

 

 
Alex Guo
Reply

 Thank you, this helped a bit.

case "ProductGroup":

                try {

                    var groups = Dynamicweb.Ecommerce.Services.ProductGroups.GetGroups();

                    if (groups != null && groups.Any()) {

                        return string.Join("|", groups.Select(g => g.Name));

                    }

                    return "";

 This shows all the ProductGroups at once, how do  I display them per product.
Also where can I find this info.
I can't find things like 'Dynamicweb.Ecommerce.Services.ProductGroups.GetGroups()' anywhere on the docs.

 
Imar Spaanjaars Dynamicweb Employee
Imar Spaanjaars
Reply

You can find API docs here:

DW9: https://doc.dynamicweb.com/documentation-9/api/apis/dw-9-12

DW10: https://doc.dynamicweb.dev/api/index.html

>>  This shows all the ProductGroups at once, how do  I display them per product.

Not sure what you mean with that. Can you elaborate?

Imar

 
Alex Guo
Reply
This post has been marked as an answer

i am trying to display the product groups per product: like a bike has a group Bike and Mountain bike

I am trying to display this per product.
I have a bike, thats part of Bikes and Mountain bikes

And about the docs,
I cant find this in the docs thats why im confused: Dynamicweb.Ecommerce.Services.ProductGroups

Votes for this answer: 1
 
Imar Spaanjaars Dynamicweb Employee
Imar Spaanjaars
Reply

(Sorry, clicked Mark as answer by accident)

Docs are here: https://doc.dynamicweb.com/documentation-9/api/apis/api-docs#article=Dynamicweb.Ecommerce.Services (I searched for Services instead of ProductGroups)

If you want the group tree structure, you can use:

Services.ProductGroups.FindPath(shop, group).ToList()

which you need to give the current shop and a group (which you can get by iterating over product.Groups)

i.e

foreach (var group in product.Groups)
{
  var pathToGroup = Services.ProductGroups.FindPath(shop, group).ToList();
}

Using IntelliSense in Visual Studio and/or a debugger should give you a better insight into what's being returned.

You could also try recursively look at the ParentGroups property for each Group on a product.

Imar

 
Alex Guo
Reply

Hey,

 

The docs url gives me a 404 not found.

 

Thanks for all the info so far.

Can you help me a bit more with the ProductGroups

 

To clarify my goal a bit more:

I am trying to make a csv transformer that needs a UrlKey

That’s why I need the productGroups.
this is a UrlKey example: http://chemicar.staging.dynamicweb-cms.com/en-gb/paint-preparation-application/ssc-00-samples-spray-sample-card/white
it exists out of the language/productgroup/nameOfTheProduct/variant

This is what the feed outputs atm:

 

All i need is the product group per product

 
Imar Spaanjaars Dynamicweb Employee
Imar Spaanjaars
Reply

There isn't necessarily something like a "group per product"; a product can be in many groups and the URL can differ depending on the group it is in.

But there's a default group ID which is usually the primary group (which you can set in the backend) or othetwise the first group a product is assigned to (which is somewhat undeterministic). You could try this:

var url = $"Default.aspx?ID=123&GroupID={product.DefaultGroup.Id}&ProductID={product.Id}&VariantID={product.VariantId}";
var friendlyUrl = SearchEngineFriendlyURLs.GetFriendlyUrl(url);

 

The ID for the page (123) in this example is the ID of a page with your product catalog on it, You can hardcode it or get it using GetPageByNavigationTag:

Dynamicweb.Services.Pages.GetPageByNavigationTag(Pageview.Area.ID, "Shop");

Hope this helps,.

Imar

 

 

 
Imar Spaanjaars Dynamicweb Employee
Imar Spaanjaars
Reply

>> The docs url gives me a 404 not found.

That's weird. Looks like a bug in the docs site. Try going here: https://doc.dynamicweb.com/documentation-9/api/apis/dw-9-12 and then search for Services.

Imar

 
Alex Guo
Reply

Hi Imar,

Thanks for your response! I appreciate the help.

To clarify, I need to extract all relevant product groups per product to correctly generate structured URLs in my CSV transformer. For example, if a product belongs to both "Sanding" and "Abrasives by machine", I need to include both in the URL, like this:
http://chemicar.staging.dynamicweb-cms.com/en-gb/sanding/abrasives-by-machine/sanding-discs-yellow-paper-oe-150-mm-15-holes/800

Any guidance on the best approach would be greatly appreciated!

Best,
Alex

 

 
Imar Spaanjaars Dynamicweb Employee
Imar Spaanjaars
Reply

See above; you can loop over product.Groups and/or use FindPath

 
Alex Guo
Reply

Hi,

How can I get the shop for that

 
Imar Spaanjaars Dynamicweb Employee
Imar Spaanjaars
Reply

Depending on where you call this from, something like this should work:

var shop = Dynamicweb.Ecommerce.Services.Shops.GetShop(PageView.Current().Area.EcomShopId);

Imar

 
Alex Guo
Reply

this is what i have now:

 

case "ProductGroup":
                if (product.Groups == null || !product.Groups.Any())
                    return "no groups";

                var groupNames = new List<string>();
                foreach (var groupInfo in product.Groups)
                {
                    groupNames.Add(groupInfo.Name ?? groupInfo.Id ?? "empty");
                }
                
                return string.Join("|", groupNames);  

 

When running this, the groups return "no groups". so the product.Groups are empty?
 

 

 
Imar Spaanjaars Dynamicweb Employee
Imar Spaanjaars
Reply

I'm missing the context from where you run this in, so I can only guess, but the following works for me in a product list template on the catalog:

foreach (var product in Model.Products)
{
  foreach (var group in product.Groups)
  {
    <h1>@group.Name</h1>
    var url = $"Default.aspx?ID=4770&GroupID={group.Id}&ProductID={product.Id}";
    var friendlyUrl = Dynamicweb.Frontend.SearchEngineFriendlyURLs.GetFriendlyUrl(url);
    @friendlyUrl
  }
}

This loops over all products and for each product loops over the groups and builds a URL. You'll need to change the ID of the catalog page but everything else should just work.

Note that GetFriendlyUrl will return the full path to the last group in the URL + the product, so it'll end up like this:

/products/group1/subgroup/subsubgroup/productname

even though you only specify one group ID in the GroupID parameter. That is, you only specify the ID of the leaf-group in the tree.

I recommend you put this directly in the list template first so you can see it work before putting it in your own code. That way you can confirm the product has the proper group set up. Once you see this work and display the proper links you can embed it in your CSV template to collect all groups and join them to a pipe separated list.

Hope this helps.

Imar

 
Alex Guo
Reply

I now have the groups working, here is the code for forming the url key I needed.

switch (fieldId)
        {
            case "Id":
                return product.Id ?? "";
            case "VariantId":
                return product.VariantId ?? "";
            case "Number":
                return product.Number ?? "";
             case "Name":
                return product.Name ?? "";
                case "UrlKey":
                    string isoLang = GetIsoLanguageCode(currentLanguageId);
                    
                    string productUrlName = string.IsNullOrEmpty(product.Name) 
                        ? "" 
                        : product.Name.ToLowerInvariant()
                            .Replace(" ", "-")
                            .Replace("&", "and")
                            .Replace("'", "")
                            .Replace("\"", "")
                            .Replace(",", "")
                            .Replace(".", "")
                            .Replace("(", "")
                            .Replace(")", "");
                            
                        while (productUrlName.Contains("--"))
                        {
                            productUrlName = productUrlName.Replace("--", "-");
                        }
                        
                        productUrlName = productUrlName.Trim('-');
                    string categoryPath = "";
                    if (product.GroupPaths != null && product.GroupPaths.Any())
                    {
                        var deepestPath = product.GroupPaths
                            .Where(p => p != null && p.Any())
                            .OrderByDescending(p => p.Count)
                            .FirstOrDefault();
                            
                        if (deepestPath != null && deepestPath.Any())
                        {
                            // Create URL-friendly category path
                            var categorySegments = deepestPath
                                .Where(g => !string.IsNullOrEmpty(g.Name))
                                .Select(g => {
                                    string urlSegment = g.Name.ToLowerInvariant()
                                        .Replace(" ", "-")
                                        .Replace("&", "")
                                        .Replace("'", "")
                                        .Replace("\"", "")
                                        .Replace(",", "")
                                        .Replace(".", "")
                                        .Replace("(", "")
                                        .Replace(")", "");
                                    
                                    // Remove consecutive hyphens
                                    while (urlSegment.Contains("--"))
                                    {
                                        urlSegment = urlSegment.Replace("--", "-");
                                    }
                                    
                                    return urlSegment.Trim('-');
                                })
                                .ToList();
                            
                            categoryPath = string.Join("/", categorySegments);
                        }
                    }
                    

                    string variantName = "";
                    if (!string.IsNullOrEmpty(product.VariantName))
                    {
                        variantName = product.VariantName
                            .ToLowerInvariant()
                            .Replace(" ", "-")    
                            .Replace("&", "and")
                            .Replace("'", "")
                            .Replace("\"", "")
                            .Replace(",", "")
                            .Replace(".", "")
                            .Replace("(", "")
                            .Replace(")", "");
                            
                        while (variantName.Contains("--"))
                        {
                            variantName = variantName.Replace("--", "-");
                        }
                        
                        variantName = variantName.Trim('-');
                    }
                    
                    // Assemble the full URL
                    if (string.IsNullOrEmpty(categoryPath))
                    {
                        // No category path available, just use language and product name
                        return $"/{isoLang}/{productUrlName}";
                    }
                    else if(string.IsNullOrEmpty(variantName))
                    {
                        // Full URL with language, category path, and product name
                        return $"/{isoLang}/{categoryPath}/{productUrlName}";
                    }
                    else
                    {
                        // Full URL with language, category path, and product name
                        return $"/{isoLang}/{categoryPath}/{productUrlName}/{variantName}";
                    }  
             default:
                return "";
        }

 

Now I want to translate the name of the product based on the language of the feed. 
Whats the best way to translate this?

 

 
Imar Spaanjaars Dynamicweb Employee
Imar Spaanjaars
Reply

Hi Alex,

I think this is a bad idea; you're creating your own URLs which is risky as they need to match the ones from DynamicWeb exactly in order to be reachable.You're not taking into account things like special characters, forward slashes in group names and more so you're likely to end up with broken links. Did you try out the code I suggested to have DW build a URL for you?

>> Whats the best way to translate this?

The catalog is already context aware so you can just call your URL from the site that has a language set in settings (that is, when you already have multiple sites for each language) or specify the language ID in the query string: ?LanguageID=IdOfYourLanguage

Imar

 
Alex Guo
Reply

I know im creating my own URLs, thats what they asked me to do as an intern.
They want this because of certain reasons.

When I add feed languages, the products don't get translated. 
as you can see in the example below the name is still english.
These are a few results from the feed in German

"Product Id";"Variant Id";"Number";"Name";"UrlKey"
"AGC2034";"";"AGC 2034";"AGC 2034 - no name received from Exact";"/de/agc-2034-no-name-received-from-exact"
"PWP90";"";"PWP 90";"PWP 90 - PWP PROMO: 3  PWP 05W + 1  PWP 05T";"/de/pwp-90-pwp-promo:-3-pwp-05w-+-1-pwp-05t"
"SSP03";"";"SSP 03";"SSP 03 - Spray sample plates mix 3 x 100p. white, light grey and deep";"/de/ssp-03-spray-sample-plates-mix-3-x-100p-white-light-grey-and-deep"

 
Imar Spaanjaars Dynamicweb Employee
Imar Spaanjaars
Reply

>> They want this because of certain reasons.

I would find out what those are and make sure they are valid before getting into this, as your current solution is asking for trouble. There are a lot of things to consider when building URLs.

>> These are a few results from the feed in German

Can you provide more context? How are you specifying the language ID? Maybe you can provide some sample URLs?

   
Imar Spaanjaars Dynamicweb Employee
Imar Spaanjaars
Reply

I think yours are translating also, but you're not seeing it :-)

Spanish:

German:

It's probably because not all products are translated correctly in the backend.

Imar

 

You must be logged in to post in the forum