Callback error when in Sharepoint 2013 (2010 Compatability mode)

4 posts, 0 answers
  1. Niels
    Niels avatar
    1 posts
    Member since:
    Mar 2014

    Posted 27 Mar 2014 Link to this post

    I developed a control to sort and order modules in a document. This is done in a telerik treeview control.

    This works fine in Sharepoint 2010. Now I have the requirement to deploy this to Sharepoint 2013, but the site collection is running in "2010 compatibility mode". What happens now is, the page is loaded and I see all the controls. But after the second, sometimes the third click on any button in the treeview (does not affect other standard buttons on the page), the control disappears when trying to rebind. The code behind is executed, but in the browser console (same behaviour in IE/FF) I see errors like
    IE: window._WebForm_InitCallback() -> (SCRIPT438: Object doesn't support this
    property or method )

    FF: init.js -> too much recursion

    The control is still "available" in the page source, but seems not to be able to draw itself again. I added screenshots to show the control before and after. The browser shows a long list of maybe 100 entries "WebForm_Initcallback, init.js. This is probably the "too much recursion shown in the javascript debugger.


    I started with a 2011 Telerik.Web.UI control version, since this is an old project, but on seeing the error, updated to the latest version 2014.1.225.35. This solution works fine in Sharepoint 2010, and Sharepoint 2013 (site collection in non-compat mode)

    I can't pinpoint the error, since it's thrown in minified sharepoint javascript files. Has anyone experienced this problem before?



    ASPX
    <telerik:RadFormDecorator ID="FormDecorator1" runat="server" DecoratedControls="all"
           Skin="Telerik"></telerik:RadFormDecorator>
       <telerik:RadAjaxPanel ID="RadAjaxPanel1" LoadingPanelID="RadAjaxLoadingPanel1" runat="server">
           <asp:Button ID="btnExpandAll" runat="server" onclick="btnExpandAll_Click" Text="<%$Resources:MSH_DocumentCreatorUI,ReportGenerator_ExpandButtonText%>"/>
           <telerik:RadTreeList runat="server" ID="RadTreeList1" DataKeyNames="ID" ParentDataKeyNames="ParentChapter"
               OnNeedDataSource="RadTreeList1_NeedDataSource" OnUpdateCommand="RadTreeList1_UpdateCommand"
               OnInsertCommand="RadTreeList1_InsertCommand" OnItemCommand="RadTreeList1_ItemCommand"
               OnItemDataBound="RadTreeList1_ItemDataBound" AutoGenerateColumns="false" EditMode="InPlace"
               OnDataBinding="RadTreeList1_DataBinding" GridLines="None" NoRecordsText="<%$Resources:MSH_DocumentCreatorUI,ChapterEditor_NoRecordsText%>">
               <Columns>
                   <docCreator:CustomTreeListEditCommandColumn UniqueName="EditCommandColumn" ButtonType="ImageButton"
                       InsertText="<%$Resources:MSH_DocumentCreatorUI,ChapterEditor_SaveButtonText%>"
                       AddRecordText="<%$Resources:MSH_DocumentCreatorUI,ChapterEditor_AddButtonText%>"
                       HeaderStyle-Width="200px" DeleteButtonImageUrl="/_layouts/Images/DocumentCreator/delete.png"
                       DeleteButtonID="DeleteButton" DeleteButtonCommand="Delete" DeleteButtonText="<%$Resources:MSH_DocumentCreatorUI,ChapterEditor_DeleteButtonText%>"
                       UpButtonImageUrl="/_layouts/Images/DocumentCreator/up.png" UpButtonID="UpButton"
                       UpButtonCommand="Up" UpButtonText="<%$Resources:MSH_DocumentCreatorUI,ChapterEditor_MoveUpButtonText%>"
                       DownButtonImageUrl="/_layouts/Images/DocumentCreator/down.png" DownButtonID="DownButton"
                       DownButtonCommand="Down" DownButtonText="<%$Resources:MSH_DocumentCreatorUI,ChapterEditor_MoveDownButtonText%>"
                       LeftButtonImageUrl="/_layouts/Images/DocumentCreator/left.png" LeftButtonID="LeftButton"
                       LeftButtonCommand="Left" LeftButtonText="<%$Resources:MSH_DocumentCreatorUI,ChapterEditor_MoveLeftButtonText%>"
                       RightButtonImageUrl="/_layouts/Images/DocumentCreator/right.png" RightButtonID="RightButton"
                       RightButtonCommand="Right" RightButtonText="<%$Resources:MSH_DocumentCreatorUI,ChapterEditor_MoveRightButtonText%>">
                   </docCreator:CustomTreeListEditCommandColumn>
                   <telerik:TreeListTemplateColumn HeaderText="<%$Resources:MSH_DocumentCreatorUI,ChapterEditor_ChapterNumberColumnHeaderText%>"
                       ItemStyle-Width="70px" HeaderStyle-Width="70px" UniqueName="ChapterNumber">
                       <ItemTemplate>
                           <asp:Label ID="lblChapterNumber" runat="server"></asp:Label>
                       </ItemTemplate>
                       <EditItemTemplate>
                           <asp:Label ID="lblChapterNumber" runat="server"></asp:Label>
                       </EditItemTemplate>
                   </telerik:TreeListTemplateColumn>
                   <telerik:TreeListBoundColumn DataField="Title" HeaderText="<%$Resources:MSH_DocumentCreatorUI,ChapterEditor_TextColumnHeaderText%>"
                       UniqueName="Title">
                   </telerik:TreeListBoundColumn>
                   <telerik:TreeListTemplateColumn HeaderText="<%$Resources:MSH_DocumentCreatorUI,ChapterEditor_LinkColumnHeaderText%>"
                       UniqueName="Link">
                       <ItemTemplate>
                           <asp:Repeater ID="textModulesRepeater" runat="server">
                               <ItemTemplate>
                                   <asp:HyperLink ID="lblLink" runat="server" Text='<%# DataBinder.Eval(Container.DataItem, "Title") %>' NavigateUrl='<%# DataBinder.Eval(Container.DataItem, "Url") %>'></asp:HyperLink>
                                   <br />
                               </ItemTemplate>
                           </asp:Repeater>
                       </ItemTemplate>
                       <EditItemTemplate>
                           <div style="overflow: auto; width: 100%; max-height: 200px; position: relative;">
                               <asp:CheckBoxList ID="textModulesList" runat="server" CssClass="textModuleCheckboxList" />
                           </div>
                       </EditItemTemplate>
                   </telerik:TreeListTemplateColumn>
                   <telerik:TreeListTemplateColumn HeaderText="<%$Resources:MSH_DocumentCreatorUI,ChapterEditor_StatusColumnHeaderText%>"
                       UniqueName="Status">
                       <ItemTemplate>
                           <asp:Image ID="Image1" ImageUrl='<%# this.GetImageUrlForColumnStatus(DataBinder.Eval(Container.DataItem, "Status")) %>'
                               runat="server" /><asp:Label ID="lblStatus" Style="vertical-align: top; margin-left: 5px"
                                   runat="server" Text='<%# this.GetLabelForColumnStatus(DataBinder.Eval(Container.DataItem, "Status")) %>'></asp:Label>
                       </ItemTemplate>
                       <EditItemTemplate>
                           <asp:DropDownList runat="server" ID="ddlStatus">
                               <asp:ListItem Text="<%$Resources:MSH_DocumentCreatorUI,StatusEnum_Green_Description%>"
                                   Value="Green"></asp:ListItem>
                               <asp:ListItem Text="<%$Resources:MSH_DocumentCreatorUI,StatusEnum_Yellow_Description%>"
                                   Value="Yellow"></asp:ListItem>
                               <asp:ListItem Text="<%$Resources:MSH_DocumentCreatorUI,StatusEnum_Red_Description%>"
                                   Value="Red" Selected="True"></asp:ListItem>
                           </asp:DropDownList>
                       </EditItemTemplate>
                   </telerik:TreeListTemplateColumn>
               </Columns>
           </telerik:RadTreeList>
       </telerik:RadAjaxPanel>
       <telerik:RadAjaxLoadingPanel ID="RadAjaxLoadingPanel1" runat="server" />


    Code Behind

    using System;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using System.Web.UI.WebControls.WebParts;
    using System.Collections;
    using Microsoft.SharePoint;
    using System.Collections.Generic;
    using Microsoft.SharePoint.Linq;
    using Telerik.Web.UI;
    using System.Data;
    using System.Linq;
    using MSH.DocumentCreator.DataAccess;
    using MSH.DocumentCreator;
    using MSH.DocumentCreator.DocumentGeneration;
     
    namespace MSH.DocumentCreator.ChapterEditor
    {
        public partial class ChapterEditorUserControl : UserControl
        {
            ReportStructure _docStructure;
            DataTable _textModules;
            IEnumerable<int> selectedTextModules;
     
            ReportStructure DocumentStructure
            {
                get
                {
                    if (_docStructure == null)
                        _docStructure = new ReportStructure();
                    return _docStructure;
                }
            }
     
            private Dictionary<int, double?> itemOrderMaxValuesByParent = new Dictionary<int, double?>();
            private Dictionary<int, double?> itemOrderMinValuesByParent = new Dictionary<int, double?>();
            private Dictionary<int, string> itemChapterNumber = new Dictionary<int, string>();
            private Dictionary<int, int> chapterParentIds = new Dictionary<int, int>();
     
            protected override void OnLoad(EventArgs e)
            {
                base.OnLoad(e);
                selectedTextModules = new List<int>();
                foreach (ChaptersItem item in DocumentStructure.Chapters)
                {
                    selectedTextModules = selectedTextModules.Union(item.LinkId.Select(l => l.Value));
                }
            }
     
            protected void btnExpandAll_Click(object sender, EventArgs e)
            {
                RadTreeList1.ExpandAllItems();
            }
     
            #region Commands
            protected void RadTreeList1_InsertCommand(object sender, TreeListCommandEventArgs e)
            {
                this.DocumentStructure.Chapters.InsertOnSubmit(GetChapter(e.Item as TreeListDataInsertItem));
                this.DocumentStructure.SaveChapters();
            }
     
            protected void RadTreeList1_UpdateCommand(object sender, TreeListCommandEventArgs e)
            {
                GetChapter(e.Item as TreeListDataItem);
                this.DocumentStructure.SaveChapters();
                ReBind();
            }
     
            public void RadTreeList1_ItemCommand(object sender, TreeListCommandEventArgs e)
            {
                if (e.CommandName == "InitInsert" || e.CommandName == "PerformInsert" || e.CommandName == "Edit" || e.CommandName == "Cancel")
                    return;
     
                ChaptersItem currentItem = GetChapter(e.Item as TreeListDataItem);
                int id = (e.Item as TreeListDataItem).GetId("ID");
                switch (e.CommandName)
                {
                    case "Left":
                        DocumentStructure.MoveLeft(currentItem);
                        break;
                    case "Right":
                        DocumentStructure.MoveRight(currentItem);
                        break;
                    case "Up":
                        DocumentStructure.MoveUp(currentItem);
                        break;
                    case "Down":
                        DocumentStructure.MoveDown(currentItem);
                        break;
                    case "Delete":
                        DocumentStructure.DeleteRecursively(currentItem);
                        DocumentStructure.SaveChapters();
                        break;
                    default:
                        break;
                }
                RadTreeList1.Rebind();
            }
            #endregion
     
            #region Binding
            protected void RadTreeList1_NeedDataSource(object sender, TreeListNeedDataSourceEventArgs e)
            {
                RadTreeList1.DataSource = DocumentStructure.Chapters.OrderBy(ch => ch.ItemOrder).ToList<ChaptersItem>();
            }
     
            protected void RadTreeList1_DataBinding(object sender, EventArgs e)
            {
                ReportParts tm = new ReportParts();
                _textModules = tm.GetByContentType("Text-Module", "Text Module");
            }
     
            public void RadTreeList1_ItemDataBound(object sender, TreeListItemDataBoundEventArgs e)
            {
                if (e.Item is TreeListHeaderItem)
                {
                    (e.Item.Controls[0].Controls[0] as Button).ToolTip = MSH.DocumentCreator.Modules.UIResources.MSH_DocumentCreatorUI.ChapterEditor_AddChapterText;
                }
                switch (e.Item.ItemType)
                {
                    case TreeListItemType.EditItem:
                        DropDownList ddlStatus = e.Item.FindControl("ddlStatus") as DropDownList;
                        ChaptersItem spItem = null;
                        if (e.Item is TreeListDataItem)
                            spItem = this.DocumentStructure.GetChapter((e.Item as TreeListDataItem).GetId("ID"));
                        if (spItem != null && spItem.Status.HasValue)
                            ddlStatus.SelectedValue = spItem.Status.Value.ToString();
                        CheckBoxList chkListTextModules = e.Item.FindControl("textModulesList") as CheckBoxList;
                        if (spItem != null)
                        {
                            var item = DocumentStructure.GetChapter(spItem.ID.Value);
                            PopulateHierarchyDictionaries(item);
                        }
                        if (chkListTextModules != null)
                        {
                            chkListTextModules.DataSource = GetUnusedOrOwnTextModules(spItem == null ? null : spItem.LinkId);
                            chkListTextModules.DataTextField = "Title";
                            chkListTextModules.DataValueField = "ID";
                            chkListTextModules.DataBind();
                        }
     
                        if (spItem != null && spItem.LinkId != null && spItem.LinkId.Count != 0)
                        {
                            foreach (int? docId in spItem.LinkId)
                            {
                                foreach (ListItem item in chkListTextModules.Items)
                                    if (docId.Value.ToString() == item.Value)
                                    {
                                        item.Selected = true;
                                    }
                            }
                        }
     
                        if (spItem != null)
                        {
                            Label lblChNumber = e.Item.FindControl("lblChapterNumber") as Label;
                            lblChNumber.Text = itemChapterNumber[spItem.ID.Value];
                        }
     
                        break;
                    case TreeListItemType.Item:
                    case TreeListItemType.AlternatingItem:
     
                        int id = (e.Item as TreeListDataItem).GetId("ID");
     
                        var currentItem = DocumentStructure.GetChapter(id);
                        var parentChapterId = currentItem.ParentChapter ?? -1;
                        PopulateHierarchyDictionaries(currentItem);
     
                        if (!itemOrderMinValuesByParent[parentChapterId].HasValue || itemOrderMinValuesByParent[parentChapterId] >= currentItem.ItemOrder)
                        {
                            ImageButton upButton = e.Item.FindControl("UpButton") as ImageButton;
                            upButton.Enabled = false;
                            upButton.ImageUrl = "/_layouts/Images/DocumentCreator/up_gray.png";
                        }
                        if (!itemOrderMaxValuesByParent[parentChapterId].HasValue || itemOrderMaxValuesByParent[parentChapterId] <= currentItem.ItemOrder)
                        {
                            ImageButton downButton = e.Item.FindControl("DownButton") as ImageButton;
                            downButton.Enabled = false;
                            downButton.ImageUrl = "/_layouts/Images/DocumentCreator/down_gray.png";
                        }
                        if (!currentItem.ParentChapter.HasValue)
                        {
                            ImageButton leftButton = e.Item.FindControl("LeftButton") as ImageButton;
                            leftButton.Enabled = false;
                            leftButton.ImageUrl = "/_layouts/Images/DocumentCreator/left_gray.png";
                        }
                        if (!DocumentStructure.Chapters.Any(ch => ch.ParentChapter == currentItem.ParentChapter && ch.ItemOrder < currentItem.ItemOrder))
                        {
                            ImageButton rightButton = e.Item.FindControl("RightButton") as ImageButton;
                            rightButton.Enabled = false;
                            rightButton.ImageUrl = "/_layouts/Images/DocumentCreator/right_gray.png";
                        }
     
                        Repeater textModulesRepeater = e.Item.FindControl("textModulesRepeater") as Repeater;
                        if (textModulesRepeater != null)
                        {
                            textModulesRepeater.DataSource = GetTextModulesWithCountry(currentItem.LinkId);
                            textModulesRepeater.DataBind();
                        }
     
                        Label lblNumber = e.Item.FindControl("lblChapterNumber") as Label;
                        lblNumber.Text = itemChapterNumber[currentItem.ID.Value];
     
                        break;
                    default:
                        break;
                }
            }
     
            protected void checkBoxList_DataBound(object sender, EventArgs args)
            {
                CheckBoxList checkBoxes = sender as CheckBoxList;
                foreach (ListItem item in checkBoxes.Items)
                {
                    item.Attributes.Add("Style", "padding: 0px !important;");
                }
            }
     
            private void ReBind()
            {
                RadTreeList1.DataSource = this.DocumentStructure.Chapters.OrderBy(ch => ch.ItemOrder);
                RadTreeList1.DataBind();
            }
            #endregion
     
            #region Helpers
     
            protected string GetImageUrlForColumnStatus(object status)
            {
                switch ((Status)status)
                {
                    case Status.Red:
                        return "/_layouts/Images/DocumentCreator/red_ball.png";
                    case Status.Green:
                        return "/_layouts/Images/DocumentCreator/green_ball.png";
                    case Status.Yellow:
                        return "/_layouts/Images/DocumentCreator/yellow_ball.png";
                    default:
                        return string.Empty;
                }
            }
     
            protected string GetLabelForColumnStatus(object status)
            {
                return Helpers.GetDescription((Status)status);
            }
     
            private ChaptersItem GetChapter(TreeListDataItem item)
            {
                return GetChapterValues(item, item.ParentItem);
     
            }
     
            private ChaptersItem GetChapter(TreeListDataInsertItem item)
            {
                return GetChapterValues(item, item.ParentItem);
            }
     
            private ChaptersItem GetChapterValues(TreeListEditableItem item, TreeListDataItem parentItem)
            {
                int? id = null;
                ChaptersItem chapter = null;
                if (item is TreeListDataItem)
                {
                    id = (item as TreeListDataItem).GetId("ID");
                    chapter = DocumentStructure.GetChapter(id.Value);
                }
                else
                    chapter = new ChaptersItem();
     
                chapter.ParentChapter = GetParentId(parentItem);
     
                Hashtable values = item.GetValues();
                DropDownList ddlStatus = item.FindControl("ddlStatus") as DropDownList;
                if (ddlStatus != null)
                {
                    Status statusValue = Status.None;
                    if (ddlStatus.SelectedValue != null)
                        statusValue = (Status)Enum.Parse(typeof(Status), ddlStatus.SelectedValue);
                    chapter.Status = statusValue;
                }
     
                CheckBoxList chkListTextModules = item.FindControl("textModulesList") as CheckBoxList;
                if (chkListTextModules != null)
                {
                    foreach (ListItem chkItem in chkListTextModules.Items)
                    {
                        int linkId = Int32.Parse(chkItem.Value);
     
                        if (chkItem.Selected)
                        {
                            if (chapter.Status.Value == Status.Red)
                                chapter.Status = Status.Yellow;
     
                            if (chapter.LinkId.Count(v => v.Value == linkId) == 0)
                                chapter.LinkId.Add(linkId);
                        }
                        else
                        {
                            if (chapter.LinkId.Count(v => v.Value == linkId) != 0)
                                chapter.LinkId.Remove(chapter.LinkId.Where(v => v.Value == linkId).First());
                        }
                    }
                }
     
                if (!chapter.ID.HasValue)
                    chapter.ItemOrder = DocumentStructure.Chapters.Where(ch => ch.ParentChapter == chapter.ParentChapter).Max(ch => ch.ItemOrder) + 1 ?? 1;
     
                chapter.Title = values["Title"] as string;
                return chapter;
            }
     
            private int? GetParentId(TreeListDataItem parent)
            {
                if (parent == null)
                    return null;
                ChaptersItem parentItem = this.DocumentStructure.GetChapter(parent.GetId("ID"));
                if (parentItem == null)
                    return null;
                return parentItem.ID;
            }
     
            private string GetChapterNumber(ChaptersItem item)
            {
                if (!item.ParentChapter.HasValue)
                {
                    return (chapterParentIds.Values.Count(v => v == -1) + 1).ToString();
                }
                else
                {
                    return itemChapterNumber[item.ParentChapter.Value] + "." + (chapterParentIds.Values.Count(v => v == item.ParentChapter.Value) + 1);
                }
            }
     
            private DataTable GetTextModulesWithCountry(IList<int?> ds)
            {
                DataTable result = new DataTable();
                result.Columns.Add("ID");
                result.Columns.Add("Title");
                result.Columns.Add("URL");
                if (ds != null)
                    foreach (DataRow module in _textModules.Rows)
                    {
                        int moduleId = (int)module["ID"];
                        if (ds.FirstOrDefault(r => r.Value == moduleId) != null)
                        {
                            var tmEntity = DocumentStructure.TextModules.FirstOrDefault(tm => tm.ID == moduleId);
                            if (tmEntity != null)
                                result.Rows.Add(moduleId, module["Title"], tmEntity.Path + "/" + tmEntity.Name);
                        }
                    }
                return result;
            }
     
            private object GetUnusedOrOwnTextModules(IList<int?> ds)
            {
                DataTable result = new DataTable();
                result.Columns.Add("ID");
                result.Columns.Add("Title");
                foreach (DataRow module in _textModules.Rows)
                {
                    if ((ds != null && ds.FirstOrDefault(r => r.Value == (int)module["ID"]) != null) || !selectedTextModules.Contains((int)module["ID"]))
                        result.Rows.Add(module["ID"], module["Title"]);
                }
                result.DefaultView.Sort = "Title";
                return result.DefaultView;
            }
     
            private void PopulateHierarchyDictionaries(ChaptersItem item)
            {
                var parentChapterId = item.ParentChapter ?? -1;
                if (!itemOrderMaxValuesByParent.ContainsKey(parentChapterId))
                    itemOrderMaxValuesByParent.Add(parentChapterId, DocumentStructure.Chapters.Where(ch => ch.ParentChapter == item.ParentChapter).Max(ch1 => ch1.ItemOrder));
     
                if (!itemOrderMinValuesByParent.ContainsKey(parentChapterId))
                    itemOrderMinValuesByParent.Add(parentChapterId, DocumentStructure.Chapters.Where(ch => ch.ParentChapter == item.ParentChapter).Min(ch1 => ch1.ItemOrder));
     
                if (!itemChapterNumber.ContainsKey(item.ID.Value))
                {
                    itemChapterNumber.Add(item.ID.Value, GetChapterNumber(item));
                }
     
                if (!chapterParentIds.ContainsKey(item.ID.Value))
                {
                    chapterParentIds.Add(item.ID.Value, parentChapterId);
                }
            }
     
            #endregion
        }
    }

  2. Marin
    Admin
    Marin avatar
    1045 posts

    Posted 01 Apr 2014 Link to this post

    Hi,

    We have addressed the support ticket that you have opened on the same issue. Here is the reply there:

    I have carefully examined the code and noticed that .DataBind() is called in the ReBind() method in order to update the controls data-source. Note that this is highly unrecommended when advanced data-bidning is used(assigning a data-source in the NeedDataSource event). Moreover manually forcing the control to repopulate after an update/insert/delete command is fired is not mandatory as RadTreeList will do this automatically. Therefore I recommend removing any .Rebind() and .DataBind() calls and testing the application again.

    Additionally I would like to ask you to remove the RadAjaxPanel that wraps the RadTreeList and test whether this has any effect.


    In order to avoid duplicate posts we can continue any further communication in the support ticket.

    Kind Regards,
    Marin
    Telerik
     

    Check out the Telerik Platform - the only platform that combines a rich set of UI tools with powerful cloud services to develop web, hybrid and native mobile apps.

     
  3. UI for ASP.NET Ajax is Ready for VS 2017
  4. Bernd
    Bernd avatar
    1 posts
    Member since:
    Jul 2011

    Posted 14 Apr 2014 in reply to Marin Link to this post

    I shouldn't have closed the ticket before I got an explanation as to WHY this helped, but for others who run into the same problem:

    I managed to find a workaround for the issue that the controls would fail to redraw while in SP2013 Compatibility Mode 14. Not by changing databind() calls or removing the ajax panel, but by adding the attribute "EnablePageHeadUpdate" and setting it to false. Previously, this wasn't set  but it's obviously set to true by default.


     <telerik:RadAjaxPanel ID="RadAjaxPanel1" LoadingPanelID="RadAjaxLoadingPanel1" runat="server"  EnablePageHeadUpdate="False">
  5. Marin
    Admin
    Marin avatar
    1045 posts

    Posted 16 Apr 2014 Link to this post

    Hi,

    Thank you for sharing this information with the community. Indeed by default when ajax request is performed the <head> tag of the page and its content is also update. This may have caused problems if the page had important scripts or styles which are used elsewhere and are not yet available when called after ajax. Setting the property to false will prevent updates of the content in the head tag and improve the behavior in such scenarios.

    Regards,
    Marin
    Telerik
     

    Check out the Telerik Platform - the only platform that combines a rich set of UI tools with powerful cloud services to develop web, hybrid and native mobile apps.

     
Back to Top