Forrest IT Services Technical Blog

Fixing the DNNBlog Permission Grid “Control Tree Must Match” Error in DNN 10

While updating DNNBlog 6.7.1 for DNN 10, I encountered an intermittent but serious error in the blog permission editor:

Failed to load ViewState. The control tree into which ViewState is being loaded must match the control tree that was used to save ViewState during the previous request.

The error appeared after changing permissions in the DNNBlog permission grid. The page could initially load correctly, but after a permission was added, removed, or updated, the following postback could fail.

The problem was not the permission data itself. It was the ViewState belonging to the dynamically generated permission-grid controls.

Why the error occurred

The DNNBlog permission grid builds its rows dynamically from the current DNN roles, users, and blog permissions.

That means the control tree can change between requests. For example:

  • A role may be added or removed.
  • A user-specific permission may be introduced.
  • A permission row may change position.
  • The number or order of generated controls may differ after an edit.

ASP.NET Web Forms normally restores child-control ViewState by position. It expects the control tree created during the new request to exactly match the tree that existed when ViewState was saved.

When the permission grid changed, ASP.NET attempted to restore old child-control ViewState into a newly generated grid with a different structure. This caused the “control tree must match” exception.

The solution was to stop saving and restoring the generated grid controls’ ViewState while continuing to preserve the information DNNBlog actually needs.

The original ViewState problem

BlogPermissionsGrid stores four items in an object array:

  1. The base or generated control ViewState
  2. The Blog ID
  3. The current user ID
  4. The serialized blog permission data

The first item was the dangerous one.

The generated permission grid is rebuilt from the current role and permission data during every request. Persisting the grid’s child-control ViewState was therefore unnecessary and unsafe.

The fix

The fix modifies two methods in:

Server\Core\Security\Controls\BlogPermissionsGrid.cs

The methods are:

LoadViewState(object savedState)

and:

SaveViewState()

The revised code deliberately leaves the first ViewState array item empty:

allStates[0] = null;

The Blog ID, current user ID, and serialized permissions are still saved normally.

The final implementation preserves the permission data but does not persist the dynamically generated child-control ViewState.

Revised LoadViewState

protected override void LoadViewState(object savedState)
{
    if (savedState == null)
    {
        return;
    }

    var myState = savedState as object[];
    if (myState == null || myState.Length < 4)
    {
        return;
    }

    // Do not restore dynamically generated child-control ViewState.
    // The permissions grid is rebuilt on each request from the
    // persisted permission data.

    if (myState[1] != null)
    {
        _BlogID = Convert.ToInt32(myState[1]);
    }

    if (myState[2] != null)
    {
        _currentUserId = Convert.ToInt32(myState[2]);
    }

    if (myState[3] != null)
    {
        var arrPermissions = new ArrayList();
        string state = Convert.ToString(myState[3]);

        if (!string.IsNullOrEmpty(state))
        {
            string[] permissionKeys = state.Split(
                new[] { "##" },
                StringSplitOptions.RemoveEmptyEntries);

            foreach (string key in permissionKeys)
            {
                string[] settings = key.Split('|');

                if (settings.Length >= 7)
                {
                    ParsePermissionKeys(settings, arrPermissions);
                }
            }
        }

        _BlogPermissions =
            new BlogPermissionCollection(arrPermissions);
    }
}

There are two important changes here.

First, the method no longer calls the base implementation to restore the generated grid’s child-control state.

Second, it still restores the values that DNNBlog requires:

_BlogID
_currentUserId
_BlogPermissions

The serialized permissions are split using the existing ## delimiter. Each permission entry is then parsed and used to rebuild the BlogPermissionCollection.

Revised SaveViewState

protected override object SaveViewState()
{
    UpdatePermissions();

    var allStates = new object[4];

    // Persist only the permission data, not the generated
    // child-control ViewState.
    allStates[0] = null;
    allStates[1] = BlogID;
    allStates[2] = CurrentUserId;

    var sb = new StringBuilder();
    bool addDelimiter = false;

    foreach (BlogPermissionInfo objBlogPermission
        in _BlogPermissions)
    {
        if (addDelimiter)
        {
            sb.Append("##");
        }
        else
        {
            addDelimiter = true;
        }

        sb.Append(
            BuildKey(
                objBlogPermission.AllowAccess,
                objBlogPermission.PermissionId,
                -1,
                objBlogPermission.RoleId,
                objBlogPermission.RoleName,
                objBlogPermission.UserId,
                objBlogPermission.DisplayName));
    }

    allStates[3] = sb.ToString();

    return allStates;
}

The important line is:

allStates[0] = null;

This prevents ASP.NET from trying to restore ViewState for a control tree that may no longer have the same rows or ordering.

The method still calls:

UpdatePermissions();

before serializing the current permission collection. It then preserves the Blog ID, current user ID, and each permission record using the existing BuildKey() format.

Why this works

The permission grid does not need its individual checkbox and row controls restored from ViewState.

Those controls can be recreated from the actual permission data.

By treating the permission collection as the source of truth, the page no longer depends on the generated grid having exactly the same control structure on every postback.

The flow becomes:

  1. Read the saved Blog ID and user ID.
  2. Restore the serialized permission records.
  3. Rebuild the permission grid from the current data.
  4. Save only the data required to build it again.

This is more reliable than attempting to preserve the complete generated control hierarchy.

What the fix does not change

The fix does not remove ViewState from the entire page.

It does not change:

  • Blog permission types
  • Role-based permissions
  • User-specific permissions
  • The existing permission serialization format
  • UpdatePermissions()
  • BuildKey()
  • ParsePermissionKeys()

It only prevents the dynamic permission-grid child controls from being stored and restored as part of ViewState.

Applying the fix safely

Before replacing the file, keep a copy of the original source and compiled module DLL.

After changing BlogPermissionsGrid.cs:

  1. Rebuild the DNNBlog project.
  2. Deploy the newly compiled DNNBlog assembly.
  3. Restart or recycle the DNN application.
  4. Clear the DNN cache.
  5. Open the blog settings and permission editor.
  6. Change a role permission and save.
  7. Reopen the permission editor.
  8. Test adding and removing a user-specific permission.
  9. Confirm that the permission values remain correct after each postback.

Because this change affects a security-related control, it should first be tested on a staging site or a test copy of the portal.

Result

With the generated child-control ViewState excluded, the permission grid is free to rebuild itself from the current DNN roles, users, and permission data.

The permission values remain persisted, but ASP.NET is no longer asked to map stale ViewState onto a changed control tree.

The result is a stable DNNBlog permission editor that can survive permission changes without throwing the Web Forms “control tree must match” exception.

Final takeaway

Dynamic Web Forms controls and ViewState can be a fragile combination.

When a control tree is rebuilt from database data on every request, saving the generated controls’ positional ViewState may cause more problems than it solves.

For the DNNBlog permission grid, the reliable approach was simple:

Persist the permission data, not the generated control tree.