In the previous posts, we covered the basics of building extensions and styling them professionally. Now let's dive into one of the most powerful patterns for working with data in extensions: the proxy pattern.

Understanding the Proxy Pattern

When working with Mendix model elements, you're not working with the actual objects directly. Instead, you work with proxies - lightweight references that fetch data on demand. This approach:

  • Improves performance - Only loads data when needed
  • Reduces memory usage - Doesn't keep entire model in memory
  • Enables lazy loading - Fetch related data as required

How Proxies Work

When you retrieve a model element, you get a proxy object:

const entity = await module.getEntityByName("Customer");
// 'entity' is a proxy, not the full entity data

To access properties, you need to explicitly load them:

// Load specific properties
const name = await entity.getName();
const attributes = await entity.getAttributes();

Loading Strategies

1. Load on Demand

Load properties only when needed:

async function displayEntity(entity) {
    const name = await entity.getName();
    console.log(`Entity: ${name}`);
    
    // Only load attributes if user requests them
    if (userWantsDetails) {
        const attributes = await entity.getAttributes();
        console.log(`Attributes: ${attributes.length}`);
    }
}

2. Batch Loading

Load multiple properties at once to reduce API calls:

async function getEntityInfo(entity) {
    // Load multiple properties in parallel
    const [name, attributes, associations] = await Promise.all([
        entity.getName(),
        entity.getAttributes(),
        entity.getAssociations()
    ]);
    
    return { name, attributes, associations };
}

3. Recursive Loading

Load nested structures efficiently:

async function loadModuleStructure(module) {
    const entities = await module.getEntities();
    
    // Load all entity details in parallel
    const entityDetails = await Promise.all(
        entities.map(async entity => ({
            name: await entity.getName(),
            attributes: await entity.getAttributes(),
            attributeNames: await Promise.all(
                (await entity.getAttributes()).map(attr => attr.getName())
            )
        }))
    );
    
    return entityDetails;
}

Practical Example: Entity Browser

Let's build an entity browser that efficiently loads and displays entities:

class EntityBrowser {
    constructor() {
        this.cache = new Map();
    }
    
    async loadEntities(moduleName) {
        // Check cache first
        if (this.cache.has(moduleName)) {
            return this.cache.get(moduleName);
        }
        
        const project = await mx.project.get();
        const module = await project.getModuleByName(moduleName);
        const entities = await module.getEntities();
        
        // Load basic info for all entities
        const entityList = await Promise.all(
            entities.map(async entity => ({
                proxy: entity,
                name: await entity.getName(),
                id: await entity.getId()
            }))
        );
        
        // Cache the results
        this.cache.set(moduleName, entityList);
        return entityList;
    }
    
    async loadEntityDetails(entityProxy) {
        // Load detailed information only when needed
        const [name, attributes, associations, documentation] = await Promise.all([
            entityProxy.getName(),
            entityProxy.getAttributes(),
            entityProxy.getAssociations(),
            entityProxy.getDocumentation()
        ]);
        
        // Load attribute details
        const attributeDetails = await Promise.all(
            attributes.map(async attr => ({
                name: await attr.getName(),
                type: await attr.getType(),
                isRequired: await attr.isRequired()
            }))
        );
        
        return {
            name,
            attributes: attributeDetails,
            associationCount: associations.length,
            documentation
        };
    }
    
    clearCache() {
        this.cache.clear();
    }
}

Using the Entity Browser

<!DOCTYPE html>
<html>
<head>
    <title>Entity Browser</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div class="mx-card">
        <div class="mx-card-header">Entity Browser</div>
        
        <select id="moduleSelect" class="mx-input">
            <option>Select a module...</option>
        </select>
        
        <div id="entityList"></div>
        <div id="entityDetails"></div>
    </div>
    
    <script type="module">
        import { mx } from "mendix";
        
        const browser = new EntityBrowser();
        
        // Load modules
        async function loadModules() {
            const project = await mx.project.get();
            const modules = await project.getModules();
            
            const select = document.getElementById('moduleSelect');
            for (const module of modules) {
                const name = await module.getName();
                const option = document.createElement('option');
                option.value = name;
                option.textContent = name;
                select.appendChild(option);
            }
        }
        
        // Load entities when module selected
        document.getElementById('moduleSelect').addEventListener('change', async (e) => {
            const moduleName = e.target.value;
            if (!moduleName) return;
            
            const entities = await browser.loadEntities(moduleName);
            displayEntityList(entities);
        });
        
        function displayEntityList(entities) {
            const list = document.getElementById('entityList');
            list.innerHTML = entities.map(entity => `
                <div class="entity-item" data-entity-id="${entity.id}">
                    ${entity.name}
                </div>
            `).join('');
            
            // Add click handlers
            list.querySelectorAll('.entity-item').forEach(item => {
                item.addEventListener('click', async () => {
                    const entityId = item.dataset.entityId;
                    const entity = entities.find(e => e.id === entityId);
                    const details = await browser.loadEntityDetails(entity.proxy);
                    displayEntityDetails(details);
                });
            });
        }
        
        function displayEntityDetails(details) {
            const detailsDiv = document.getElementById('entityDetails');
            detailsDiv.innerHTML = `
                <h3>${details.name}</h3>
                <p><strong>Attributes:</strong> ${details.attributes.length}</p>
                <p><strong>Associations:</strong> ${details.associationCount}</p>
                <div class="attributes">
                    ${details.attributes.map(attr => `
                        <div class="attribute">
                            ${attr.name} (${attr.type})
                            ${attr.isRequired ? '<span class="required">*</span>' : ''}
                        </div>
                    `).join('')}
                </div>
            `;
        }
        
        loadModules();
    </script>
</body>
</html>

Performance Optimization Tips

1. Cache Aggressively

class ProxyCache {
    constructor() {
        this.cache = new Map();
    }
    
    async getOrLoad(key, loader) {
        if (this.cache.has(key)) {
            return this.cache.get(key);
        }
        
        const value = await loader();
        this.cache.set(key, value);
        return value;
    }
}

2. Debounce Expensive Operations

function debounce(func, wait) {
    let timeout;
    return function executedFunction(...args) {
        clearTimeout(timeout);
        timeout = setTimeout(() => func(...args), wait);
    };
}

const debouncedSearch = debounce(async (query) => {
    // Expensive search operation
    const results = await searchEntities(query);
    displayResults(results);
}, 300);

3. Use Virtual Scrolling for Large Lists

For displaying hundreds of entities, implement virtual scrolling to only render visible items.

Error Handling with Proxies

Always handle errors when working with proxies:

async function safeLoadEntity(entityProxy) {
    try {
        const name = await entityProxy.getName();
        const attributes = await entityProxy.getAttributes();
        return { name, attributes };
    } catch (error) {
        console.error('Failed to load entity:', error);
        return null;
    }
}

Best Practices

  • Load in parallel - Use Promise.all() for independent operations
  • Cache intelligently - Cache expensive operations but invalidate when needed
  • Show loading states - Let users know when data is being fetched
  • Handle errors gracefully - Proxies can fail if model elements are deleted
  • Minimize round trips - Batch related property loads together

Conclusion

The proxy pattern is essential for building performant Mendix Studio Pro extensions. By understanding how to efficiently load and cache data, you can create powerful tools that feel responsive and professional.

With the knowledge from this series, you're now equipped to build sophisticated extensions that enhance your Mendix development workflow. Happy coding!