'Map key null not found in map' when using apex:pageBlockTable

Things often get a bit hairy when you start to use maps of maps (or mapception :D). You can represent tables of data in this way if you change your variable model.

Change the map of maps to be a list of a new inner class that contains simply the name of the inner property and a map of the outer properties to boolean values. Also add a method to retrieve a list of the column names, like so:

public class UserExtension
{
    private final User theUser;

    public UserExtension(ApexPages.StandardController stdController)
    {
        this.theUser = (User)stdController.getRecord();
    }

    public List<String> getColumns()
    {
        return new List<String>{'Outer1','Outer2'};
    }

    public List<innerClass> getMap()
    {    
        List<innerClass> theMap = new List<innerClass>{};
        theMap.add(new innerClass('Inner1', new Map<String, Boolean> { 'Outer1' => true, 'Outer2' => false}));
        theMap.add(new innerClass('Inner2', new Map<String, Boolean> { 'Outer1' => true, 'Outer2' => true }));
        return theMap;
    }

    public class innerClass
    {
        public String Name {get;set;}
        public Map<String, Boolean> outers {get;set;}

        public innerClass(String Name, Map<String, Boolean> outers)
        {
            this.Name = Name;
            this.outers = outers;
        }
    }
}

With this in place, then your page code changes to the following:

<apex:page standardController="User" extensions="UserExtension">
<apex:pageBlock >
    <apex:pageBlockTable value="{!Map}" var="innerKey">
        <apex:column value="{!innerKey.Name}"/>
        <apex:repeat value="{!columns}" var="col">
            <apex:column headerValue="{!col}">
                {!innerKey.outers[col]}
            </apex:column>
        </apex:repeat>
    </apex:pageBlockTable>
</apex:pageBlock>

Your table should now generate as required.


Here is a work around for your problem.

Add null key in controller

Map<String, Map<String, Boolean>> theMap = new Map<String, Map<String, Boolean>>();
        theMap.put('Outer 1', new Map<String, Boolean> { 'Inner 1' => true, 'Inner 2' => false });
        theMap.put('Outer 2', new Map<String, Boolean> { 'Inner 1' => true, 'Inner 2' => true });
        theMap.put(null, new Map<String, Boolean> { 'Inner 1' => true, 'Inner 2' => true });
        return theMap;

Do this modification inside VF page

<apex:page standardController="User" extensions="UserExtension">
    <apex:pageBlock >
        <apex:pageBlockTable value="{!Map}" var="outerKey">
            <apex:column rendered="{! outerKey != null }" value="{!outerKey}"/>
            <apex:repeat rendered="{! outerKey != null }" value="{!Map[outerKey]}" var="innerKey">
                <apex:column  rendered="{! outerKey != null }" headerValue="{!innerKey}">
                    {!Map[outerKey][innerKey]}
                </apex:column>
            </apex:repeat>
        </apex:pageBlockTable>
    </apex:pageBlock>
</apex:page>

The page block will start working.


One workaround I have found (which keeps the column headers intact) so far is to add a getHeaders method to my controller and change my apex:repeat definition.

Whilst this works perfectly in this situation (since I know the size of the inner map will always be the same), it doesn't feel like a very clean solution.

Controller

public class UserExtension
{
    private final User theUser;

    public UserExtension(ApexPages.StandardController stdController)
    {
        this.theUser = (User)stdController.getRecord();
    }

    public List<String> getHeaders()
    {
        return new List<String>(getMap().values().get(0).keySet());
    }

    public Map<String, Map<String, Boolean>> getMap()
    {
        Map<String, Map<String, Boolean>> theMap = new Map<String, Map<String, Boolean>>();
        theMap.put('Outer 1', new Map<String, Boolean> { 'Inner 1' => true, 'Inner 2' => false });
        theMap.put('Outer 2', new Map<String, Boolean> { 'Inner 1' => true, 'Inner 2' => true });
        theMap.put('Outer 3', new Map<String, Boolean> { 'Inner 1' => true, 'Inner 2' => true });
        theMap.put('Outer 4', new Map<String, Boolean> { 'Inner 1' => true, 'Inner 2' => false });
        return theMap;
    }
}

Page

<apex:page standardController="User" extensions="UserExtension">
    <apex:pageBlock >
        <apex:pageBlockTable value="{!Map}" var="outerKey">
            <apex:column value="{!outerKey}"/>
            <apex:repeat value="{!IF(outerKey != null, PermissionsMap[outerKey], Headers)}" var="innerKey">
                <apex:column headerValue="{!innerKey}">
                    {!Map[outerKey][innerKey]}
                </apex:column>
            </apex:repeat>
        </apex:pageBlockTable>
    </apex:pageBlock>
</apex:page>