序列化时JSON.NET StackOverflowException

时间:2017-01-24 12:14:45

标签: c# serialization json.net stack-overflow

当我尝试序列化具有类似结构的对象时,我的C#程序运行到StackOverflowException:

  • 对象具有相互引用的成员
  • 无法尝试抓住(idk why)
  • 如果计数设置低于6500(可能因机器而异),则会成功序列化

以下示例代码:

class Chacha
{
    public Chacha NextChacha { get; set; }
}    
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.All,
    PreserveReferencesHandling = PreserveReferencesHandling.Objects,
    ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};

static void Main(string[] args)
{
        int count = 15000;

        Chacha[] steps = new Chacha[count];
        steps[0] = new Chacha();

        for (int i = 1; i < count; i++)
        {
            steps[i] = new Chacha();
            steps[i-1].NextChacha = steps[i];
        }

        string serSteps = JsonConvert.SerializeObject(steps, Settings);
}

JSON.NET版本是:9.0.1
.NET Framework:4.5.2
如何序列化这种结构的任何解决方案?

欢迎任何帮助或建议。谢谢

4 个答案:

答案 0 :(得分:5)

您获得stackoverflow异常的原因是Json.NET是一个递归的单遍树或图形序列化程序,在启用PreserveReferencesHandling.Objects时,始终序列化第一个事件每个对象。您已构造了15,000个元素Chacha []数组,以便第一个条目是包含按顺序链接的所有其他项的链接列表的头部。 Json.NET将尝试通过15,000级递归将序列化为嵌套的JSON对象15,000级,在此过程中溢出堆栈。

因此,您需要做的是仅在列表的 head 处编写整个链接表,作为JSON数组。不幸的是,Json.NET也是一个基于契约的序列化器,这意味着无论嵌套深度是什么,它都会在遇到给定类型的对象时尝试编写相同的属性。因此,向Chacha[] NextChachaList对象添加Chacha属性无济于事,因为它将在每个级别编写。相反,有必要创建一个相当复杂的custom JsonConverter,以线程安全的方式跟踪序列化深度,并且只在顶层写入链接列表。以下是诀窍:

class ChachaConverter : LinkedListItemConverter<Chacha>
{
    protected override bool IsNextItemProperty(JsonProperty member)
    {
        return member.UnderlyingName == "NextChacha"; // Use nameof(Chacha.NextChacha) in latest c#
    }
}

public abstract class LinkedListItemConverter<T> : JsonConverter where T : class
{
    const string refProperty = "$ref";
    const string idProperty = "$id";
    const string NextItemListProperty = "nextItemList";

    [ThreadStatic]
    static int level;

    // Increments the nesting level in a thread-safe manner.
    int Level { get { return level; } set { level = value; } }

    public override bool CanConvert(Type objectType)
    {
        return typeof(T).IsAssignableFrom(objectType);
    }

    protected abstract bool IsNextItemProperty(JsonProperty member);

    List<T> GetNextItemList(object value, JsonObjectContract contract)
    {
        var property = contract.Properties.Where(p => IsNextItemProperty(p)).Single();
        List<T> list = null;
        for (var item = (T)property.ValueProvider.GetValue(value); item != null; item = (T)property.ValueProvider.GetValue(item))
        {
            if (list == null)
                list = new List<T>();
            list.Add(item);
        }
        return list;
    }

    void SetNextItemLinks(object value, List<T> list, JsonObjectContract contract)
    {
        var property = contract.Properties.Where(p => IsNextItemProperty(p)).Single();
        if (list == null || list.Count == 0)
            return;
        var previous = value;
        foreach (var next in list)
        {
            if (next == null)
                continue;
            property.ValueProvider.SetValue(previous, next);
            previous = next;
        }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        using (new PushValue<int>(Level + 1, () => Level, (old) => Level = old))
        {
            writer.WriteStartObject();

            if (serializer.ReferenceResolver.IsReferenced(serializer, value))
            {
                writer.WritePropertyName(refProperty);
                writer.WriteValue(serializer.ReferenceResolver.GetReference(serializer, value));
            }
            else
            {
                writer.WritePropertyName(idProperty);
                writer.WriteValue(serializer.ReferenceResolver.GetReference(serializer, value));

                var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(value.GetType());

                // Write the data properties (if any).
                foreach (var property in contract.Properties
                    .Where(p => p.Readable && !p.Ignored && (p.ShouldSerialize == null || p.ShouldSerialize(value))))
                {
                    if (IsNextItemProperty(property))
                        continue;
                    var propertyValue = property.ValueProvider.GetValue(value);
                    if (propertyValue == null && serializer.NullValueHandling == NullValueHandling.Ignore)
                        continue;
                    writer.WritePropertyName(property.PropertyName);
                    serializer.Serialize(writer, propertyValue);
                }

                if (Level == 1)
                {
                    // Write the NextItemList ONLY AT THE TOP LEVEL
                    var nextItems = GetNextItemList(value, contract);
                    if (nextItems != null)
                    {
                        writer.WritePropertyName(NextItemListProperty);
                        serializer.Serialize(writer, nextItems);
                    }
                }
            }
            writer.WriteEndObject();
        }
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        var jObject = JObject.Load(reader);

        // Detach and process $ref
        var refValue = (string)jObject[refProperty].RemoveFromLowestPossibleParent();
        if (refValue != null)
        {
            var reference = serializer.ReferenceResolver.ResolveReference(serializer, refValue);
            if (reference != null)
                return reference;
        }

        // Construct the value
        var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(existingValue == null ? typeof(T) : existingValue.GetType());
        T value = (existingValue as T ?? (T)contract.DefaultCreator());

        // Detach and process $id
        var idValue = (string)jObject[idProperty].RemoveFromLowestPossibleParent();
        if (idValue != null)
        {
            serializer.ReferenceResolver.AddReference(serializer, idValue, value);
        }

        // Detach the (possibly large) list of next items.
        var nextItemList = jObject[NextItemListProperty].RemoveFromLowestPossibleParent();

        // populate the data properties (if any)
        serializer.Populate(jObject.CreateReader(), value);

        // Set the next item references
        if (nextItemList != null)
        {
            var list = nextItemList.ToObject<List<T>>(serializer);
            SetNextItemLinks(value, list, contract);
        }

        return value;
    }
}

public struct PushValue<T> : IDisposable
{
    Action<T> setValue;
    T oldValue;

    public PushValue(T value, Func<T> getValue, Action<T> setValue)
    {
        if (getValue == null || setValue == null)
            throw new ArgumentNullException();
        this.setValue = setValue;
        this.oldValue = getValue();
        setValue(value);
    }

    #region IDisposable Members

    // By using a disposable struct we avoid the overhead of allocating and freeing an instance of a finalizable class.
    public void Dispose()
    {
        if (setValue != null)
            setValue(oldValue);
    }

    #endregion
}

public static class JsonExtensions
{
    public static JToken RemoveFromLowestPossibleParent(this JToken node)
    {
        if (node == null)
            return null;
        var contained = node.AncestorsAndSelf().Where(t => t.Parent is JContainer && t.Parent.Type != JTokenType.Property).FirstOrDefault();
        if (contained != null)
            contained.Remove();
        // Also detach the node from its immediate containing property -- Remove() does not do this even though it seems like it should
        if (node.Parent is JProperty)
            ((JProperty)node.Parent).Value = null;
        return node;
    }
}

然后,给出略微修改的类Chacha

class Chacha
{
    public Chacha NextChacha { get; set; }

    public long Data { get; set; }
}

为3个项目的数组生成以下JSON:

{
  "$type": "Question41828014.Chacha[], Tile",
  "$values": [
    {
      "$id": "1",
      "Data": 0,
      "nextItemList": {
        "$type": "System.Collections.Generic.List`1[[Question41828014.Chacha, Tile]], mscorlib",
        "$values": [
          {
            "$id": "2",
            "Data": 1
          },
          {
            "$id": "3",
            "Data": 2
          }
        ]
      }
    },
    {
      "$ref": "2"
    },
    {
      "$ref": "3"
    }
  ]
}

请注意,JSON深度现在受到严格限制。示例fiddle

请注意,一旦为类型指定了自定义转换器,它就需要手动完成所有操作。如果您的类型Chacha是多态的,并且您需要读取和写入"$type"属性,则需要自己将该逻辑添加到转换器。

顺便说一句,我建议TypeNameHandling.Objects而不是TypeNameHandling.All。可以在JSON中合理地指定对象类型(只要类型正确sanitized),但应在代码中指定集合类型。这样做可以从数组切换到List<T>,而无需发布旧版JSON文件。

答案 1 :(得分:1)

MVC 与 Jquery:

通过控制台nuget包安装newtonsoft包: 安装包 Newtonsoft.json -版本 6.0.1

控制器:

[HttpPost]
    public ActionResult Get_Country()
    {
        string _data = "";
        SqlCommand cmd = new SqlCommand("Get_Country", con);
        cmd.CommandType = CommandType.StoredProcedure;
        SqlDataAdapter da = new SqlDataAdapter(cmd);
        DataSet ds = new DataSet();
        da.Fill(ds);
        if (ds.Tables[0].Rows.Count > 0)
        {
            _data = JsonConvert.SerializeObject(ds.Tables[0]);
        }
        return Json(_data, JsonRequestBehavior.AllowGet);
    }

    [HttpPost]
    public ActionResult Insert(string A,string B,string C,string D)
    {
        con.Open();
        SqlCommand cmd = new SqlCommand("Insert_Employee", con);
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.Parameters.AddWithValue("@Name", A);
        cmd.Parameters.AddWithValue("@CID", B);
        cmd.Parameters.AddWithValue("@Gender", C);
        cmd.Parameters.AddWithValue("@Hobbies", D);
        int count = cmd.ExecuteNonQuery();
        con.Close();
        return View();
    }

    [HttpPost]
    public ActionResult Get_Employees()
    {
        string _data = "";
        SqlCommand cmd = new SqlCommand("Get_Employees", con);
        cmd.CommandType = CommandType.StoredProcedure;
        SqlDataAdapter da = new SqlDataAdapter(cmd);
        DataSet ds = new DataSet();
        da.Fill(ds);
        if (ds.Tables[0].Rows.Count > 0)
        {
            _data = JsonConvert.SerializeObject(ds.Tables[0]);
        }
        return Json(_data, JsonRequestBehavior.AllowGet);
    }

    [HttpPost]
    public ActionResult EditData(int A)
    {
        string _data = "";
        SqlCommand cmd = new SqlCommand("Get_Employees_Edit", con);
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.Parameters.AddWithValue("@ID", A);
        SqlDataAdapter da = new SqlDataAdapter(cmd);
        DataSet ds = new DataSet();
        da.Fill(ds);
        if (ds.Tables[0].Rows.Count > 0)
        {
            _data = JsonConvert.SerializeObject(ds.Tables[0]);
        }
        return Json(_data, JsonRequestBehavior.AllowGet);
    }

查看:

脚本:

<script type="text/javascript">

    var IDD = "";
            
    $(document).ready(function () {
        Get_Country();
        GetEmployees();
    });

    function Get_Country() {
        $.ajax({
            url: 'Employee/Get_Country',
            type: 'post',
            data: {},
            success: function (_dt) {
                _dt = JSON.parse(_dt);
                for (var i = 0; i < _dt.length; i++) {
                    $('#ddlCountry').append($('<option/>').attr('value',_dt[i].CID).text(_dt[i].CName));
                }
            },
            error: function () {
                alert('Error in Country Bind');
            }
        })
    }

    function SaveData() {
        debugger    
        var HOB = "";
        if ($("#btnsave").val() == ("Save")) {
            HOB = $('input:checkbox:checked.B').map(function () {
                return this.value;
            }).get().join(',');
            $.ajax({
                url: 'Employee/Insert',
                type: 'post',
                data: { A: $('#txtName').val(), B: $('#ddlCountry').val(), C: $('input:radio[name=A]:checked').val(), D: HOB },
                success: function () {
                    alert('data saved');
                    GetEmployees();
                },
                error: function () {
                    alert('saved error');
                }
            });
        }
    }


    function GetEmployees() {
        debugger
        $.ajax({
            url: 'Employee/Get_Employees',
            type: 'post',
            data: {},
            async: false,
            success: function (_dt) {
                _dt = JSON.parse(_dt);
                $('#tbl').find("tr:gt(0)").remove();
                for (var i = 0; i < _dt.length; i++) {
                    $('#tbl').append('<tr><td>' + _dt[i].ID + '</td><td>' + _dt[i].Name + '</td><td>' + _dt[i].CName + '</td><td>' + (_dt[i].Gender == 1 ? 'Male' : _dt[i].Gender == 2 ? 'Female' : 'Others') + '</td><td>' + _dt[i].Hobbies + '</td><td><input id="btnEdit" type="button" value="Edit" onclick="EditData(' + _dt[i].ID + ')"/></td><td><input id="btnDelete" type="button" value="Delete" onclick="DeleteData(' + _dt[i].ID + ')"/></td></tr>')
                }
            },
            error: function () {
                alert('error in binding');
            }
        });
    }
    


    function EditData(id) {
        debugger;
        $.ajax({
            url: 'Employee/EditData',
            type: 'post',
            data: { A: id },
            success: function (_dt) {
                _dt = JSON.parse(_dt);
                debugger;
                $("#txtName").val(_dt[0].Name);
                $("#ddlCountry").val(_dt[0].CID);
                $("input[name=A][value=" + _dt[0].Gender + "]").prop('checked', true);

                var hbb = _dt[0].Hobbies;
                var arr = hbb.split(',');
                $('input:checkbox:checked.B').prop('checked', false);
                for (var i = 0; i < arr.length; i++) {
                    $('input:checkbox[class=B][value=' + arr[i] + ']').prop('checked', true);
                }

                $("#btnsave").val("Update");
                IDD = id;
            },
            error: function () {
                alert('edit error !!');
            }
        });
    }

身体:

     <table>
        <tr>
            <td>Name :</td>
            <td>
                <input id="txtName" type="text" />
            </td>
        </tr>
        <tr>
            <td>Country :</td>
            <td>
                <select id="ddlCountry">
                    <option value="0"><--Select--></option>
                </select>
            </td>
        </tr>
        <tr>
            <td>Gender :</td>
            <td>
                <input type="radio" name="A" value="1" />Male
                <input type="radio" name="A" value="2" />Female
                <input type="radio" name="A" value="3" />Others
            </td>
        </tr>
        <tr>
            <td>Hobbies :</td>
            <td>
                <input type="checkbox" class="B" value="Cricket" />Cricket
                <input type="checkbox" class="B" value="Coding" />Coding
                <input type="checkbox" class="B" value="Travelling" />Travelling
                <input type="checkbox" class="B" value="Swimming" />Swimming
                <input type="checkbox" class="B" value="Movies" />Movies
                <input type="checkbox" class="B" value="Typing" />Typing
            </td>
        </tr>
        <tr>
            <td></td>
            <td>
                <input type="button" id="btnsave" value="Save" onclick="SaveData()" />
            </td>
        </tr>
    </table>

    <table id="tbl" border="1">
        <tr>
            <td>ID</td>
            <td>Name</td>
            <td>Country</td>
            <td>Gender</td>
            <td>Hobbies</td>
            <td>Edit</td>
            <td>Delete</td>
        </tr>
    </table>

答案 2 :(得分:0)

public class Model
 {
    public int Id { get; set; }
    public string Name { get; set; }
    public string SurName { get; set; }
 }
List<Model> list = new List<Model>();
list.Add(new Model { Id = 1, Name = "Jon", SurName = "Snow"});
var stringJson = JsonConvert.SerializeObject(list, new JsonSerializerSettings
  {
    PreserveReferencesHandling = PreserveReferencesHandling.Objects
  });

答案 3 :(得分:0)

Jquery cs code:

```csharp
SqlConnection con = new SqlConnection(ConfigurationManager.ConnectionStrings["DBCS"].ConnectionString);

[WebMethod]
public string GetCountry()
{
    string _data = "";
    con.Open();
    SqlCommand cmd = new SqlCommand("usp_country_get",con);
    cmd.CommandType = CommandType.StoredProcedure;
    SqlDataAdapter da = new SqlDataAdapter(cmd);
    DataSet ds = new DataSet();
    da.Fill(ds);
    con.Close();
    if (ds.Tables[0].Rows.Count > 0)
    {
        _data = JsonConvert.SerializeObject(ds.Tables[0]);
    }
    return _data;
}

[WebMethod]
public void Insert(string A, int B, int C, string D)
{
    con.Open();
    SqlCommand cmd = new SqlCommand("usp_emp_insert", con);
    cmd.CommandType = CommandType.StoredProcedure;
    cmd.Parameters.AddWithValue("@name", A);
    cmd.Parameters.AddWithValue("@cid", B);
    cmd.Parameters.AddWithValue("@gender", C);
    cmd.Parameters.AddWithValue("@hobbies", D);
    cmd.ExecuteNonQuery();
    con.Close();
}


[WebMethod]
public void Delete(int ID)
{
    con.Open();
    SqlCommand cmd = new SqlCommand("usp_emp_delete", con);
    cmd.CommandType = CommandType.StoredProcedure;
    cmd.Parameters.AddWithValue("@id", ID);
    cmd.ExecuteNonQuery();
    con.Close();
}

[WebMethod]
public string Edit(int ID)
{
    string _data = "";
    con.Open();
    SqlCommand cmd = new SqlCommand("usp_emp_edit", con);
    cmd.CommandType = CommandType.StoredProcedure;
    cmd.Parameters.AddWithValue("@id", ID);
    SqlDataAdapter da = new SqlDataAdapter(cmd);
    DataSet ds = new DataSet();
    da.Fill(ds);
    con.Close();
    if (ds.Tables[0].Rows.Count > 0)
    {
        _data = JsonConvert.SerializeObject(ds.Tables[0]);
    }
    return _data;
}

[WebMethod]
public void Update(int ID, string A, int B, int C, string D)
{
    con.Open();
    SqlCommand cmd = new SqlCommand("usp_emp_update", con);
    cmd.CommandType = CommandType.StoredProcedure;
    cmd.Parameters.AddWithValue("@id", ID);
    cmd.Parameters.AddWithValue("@name", A);
    cmd.Parameters.AddWithValue("@cid", B);
    cmd.Parameters.AddWithValue("@gender", C);
    cmd.Parameters.AddWithValue("@hobbies", D);
    cmd.ExecuteNonQuery();
    con.Close();
}

[WebMethod]
public string GetEmployee()
{
    string _data = "";
    con.Open();
    SqlCommand cmd = new SqlCommand("usp_emp_get", con);
    cmd.CommandType = CommandType.StoredProcedure;
    SqlDataAdapter da = new SqlDataAdapter(cmd);
    DataSet ds = new DataSet();
    da.Fill(ds);
    con.Close();
    if (ds.Tables[0].Rows.Count > 0)
    {
        _data = JsonConvert.SerializeObject(ds.Tables[0]);
    }
    else
    {
        _data = JsonConvert.SerializeObject(ds.Tables[0]);
    }
    return _data;
}
```







aspx code:

Scripting :

```html
<script src="jquery-3.2.1.js"></script>

<script type="text/javascript">
    var IDD = "";
    $(document).ready(function () {
        CountryBind();
        //EmployeeBind();
    });

    function CountryBind() {
        $.ajax({
            url:'Employee.asmx/GetCountry',
            type:'post',
            contentType:'application/json;charset=utf-8',
            dataType:'json',
            data:'{}',
            async: false,
            success: function (_dt) {
                _dt = JSON.parse(_dt.d);
                for (var i = 0; i < _dt.length; i++) {
                    $("#ddlcountry").append($('<option/>').attr("value", _dt[i].cid).text(_dt[i].cname));
                }
            },
            error: function () {
                alert('CountryBind error');
            }
        });
    }


    function SaveData() {
        var HOB = "";
        HOB = $('input:checkbox:checked.B').map(function () {
            return this.value;
        }).get().join(',');
        if ($("#btnsave").val()==("Save")) {
            $.ajax({
                url: 'Employee.asmx/Insert',
                type: 'post',
                contentType: 'application/json;charset=utf-8',
                dataType: 'json',
                data: "{A:'" + $("#txtname").val() + "',B:'" + $("#ddlcountry").val() + "',C:'" + $('input:radio[name=A]:checked').val() + "',D:'" + HOB + "'}",
                success: function () {
                    alert('insert success !!');
                    EmployeeBind();
                },
                error: function () {
                    alert('insert error !!');
                }
            });
        }
        else {
            $.ajax({
                url: 'Employee.asmx/Update',
                type: 'post',
                contentType: 'application/json;charset=utf-8',
                dataType: 'json',
                data: "{ID:" + IDD + ",A:'" + $("#txtname").val() + "',B:'" + $("#ddlcountry").val() + "',C:'" + $('input:radio[name=A]:checked').val() + "',D:'" + HOB + "'}",
                success: function () {
                    alert('Update Successfully');
                    EmployeeBind();
                },
                error: function () {
                    alert('Update error');
                }
            });
        }
    }

    function EmployeeBind() {
        $.ajax({
            url: 'Employee.asmx/GetEmployee',
            type: 'post',
            contentType: 'application/json;charset=utf-8',
            dataType: 'json',
            data: "{}",
            async: false,
            success: function (_dt) {
                _dt = JSON.parse(_dt.d);
                $("#tbl").find("tr:gt(0)").remove();
                for (var i = 0; i < _dt.length; i++) {
                    $("#tbl").append('<tr><td>' + _dt[i].name + '</td><td>' + _dt[i].cname + '</td><td>' + (_dt[i].gender == "1" ? "Male" : _dt[i].gender == "2" ? "FeMale" : "Others") + '</td><td>' + _dt[i].hobbies + '</td><td><input type="button" id="btnedit" value="Edit" onclick="EditData(' + _dt[i].id + ')" /><td><input type="button" id="btndelete" value="Delete" onclick="DeleteData(' + _dt[i].id + ')" /></td</tr>');
                }
            },
            error: function () {
                alert('EmployeeBind error');
            }
        });
    }

    function DeleteData(id) {
        $.ajax({
            url: 'Employee.asmx/Delete',
            type: 'post',
            contentType: 'application/json;charset=utf-8',
            dataType: 'json',
            data: "{ID:'" + id + "'}",
            success: function () {
                alert('Delete Successfully');
                EmployeeBind();
            },
            error: function () {
                alert('DeleteData error');
            }
        });
    }

    function EditData(id) {
        $.ajax({
            url: 'Employee.asmx/Edit',
            type: 'post',
            contentType: 'application/json;charset=utf-8',
            dataType: 'json',
            async:false,
            data: "{ID:'" + id + "'}",
            success: function (_dt) {
                _dt = JSON.parse(_dt.d);
                $("#txtname").val(_dt[0].name);
                $("#ddlcountry").val(_dt[0].cid);
                $("input[name=A][value=" + _dt[0].gender + "]").prop('checked', true);

                var hbb = _dt[0].hobbies;
                var arr = hbb.split(',');
                $("input:checkbox:checked.B").prop('checked', false);
                for (var i = 0; i < arr.length; i++) {
                    $("input:checkbox[class=B][value=" + arr[i] + ']').prop('checked', true);
                }

                $("#btnsave").val("Update");
                IDD = id;
                EmployeeBind();
            },
            error: function () {
                alert('EditData error');
            }
        });
    }
</script>
```

Body:

```html
<body>
    <table>
        <tr>
            <td>Name :</td>
            <td><input type="text" id="txtname" /></td>
        </tr>
        <tr>
            <td>Country :</td>
            <td><select id="ddlcountry">
                    <option value="0">--Select--</option>
                </select>
            </td>
        </tr>
        <tr>
            <td>Gender :</td>
            <td>
                <input type="radio" name="A" value="1" /> male
                <input type="radio" name="A" value="2" /> female
                <input type="radio" name="A" value="3" /> other
            </td>
        </tr>
        <tr>
            <td>Hobbies :</td>
            <td>
                <input type="checkbox" class="B" value="cricket" /> cricket
                <input type="checkbox" class="B" value="music" /> music
                <input type="checkbox" class="B" value="movies" /> movies
                <input type="checkbox" class="B" value="cooking" /> cooking
                <input type="checkbox" class="B" value="coding" /> coding
            </td>
        </tr>
        <tr>
            <td></td>
            <td><input type="button" id="btnsave" value="Save" onclick="SaveData()" /></td>
        </tr>
    </table>
    
    <table id="tbl" border="1" style="border-color:darkblue;width:100%">
        <tr>
            <th>Name</th>
            <th>Country</th>
            <th>Gender</th>
            <th>Hobbies</th>
            <th>Edit</th>
            <th>Delete</th>
        </tr>
    </table>
</body>
```