Using Proxy for Data Retrieval in Mendix Extensions
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!