This post explores a simple application I’ve developed to streamline control name refactoring within Power Apps Canvas Apps code that save a bunch of time renaming duplicated controls in Power Apps Studio
🤦♂️ Lots of Controls to rename
Recently I got into a project were a “legacy” Canvas Power Apps. I appropriated the term here, but just to point out that the maturity of development of the app was low and don’t used any least good practices. No kidding, in some cases we got a control replicated n times, just like this:

This turned into a huge problem in our refactoring/modernization task, even to discover the current APP.
So, as I was already excited about YAML serialization possibilities I notice that at this point a cool use of it will be awesome! ✍️(◔◡◔)
🍲 Implementation
Refactoring Goals and Logic:
The primary objective is to modify control names ending in the suffix “_2,_5,_100….” (e.g., Icon5_2
). The script aims to achieve this by adhering to the following naming convention:

The control type prefix, I’m following Matthew Devaney “2024 Power Apps Coding Standards For Canvas Apps”
{control type prefix}
: This is derived from thecontrol_prefixes
dictionary (coding Standards).{context/purpose}
: Extracted from the control’sText
orTooltip
property (priority given toText
).{container/group}
: Determined by the control’s indentation level within the code.{screen}
: Retrieved from the first line of the YAML file (screen where the control is).
Example:


Understanding the Code Structure:
The script relies on a YAML file that defines the Power Apps Canvas App code schema. Additionally, I’ve established a dictionary named control_prefixes
that maps control types (e.g., button
) to their corresponding prefixes (e.g., btn
).
Key Script Steps:
- Line Reading and Control Information Extraction:
- The script reads each line of the YAML code.
- It extracts the screen name and constructs the control hierarchy based on indentation levels.
- It identifies controls with names ending that are eligible for refactoring (_2, _5,…_100…)
- Control type (e.g.,
label
) is determined.
1.1 Context/Purpose Derivation:
- The script prioritizes extracting context from the
Text
property. - If
Text
is unavailable, it attempts to use theTooltip
property
2. Refactoring and Renaming:
- If the control name qualifies for refactoring, a new name is constructed using the defined format.
- Both the original and new names are stored.
3. Reference Update:
- The script iterates through the code again, searching for references to the renamed controls (using the original names).
- These references are updated to reflect the new control names.
👨💻The Code
#made by @jean-paul-dosher
#visit: https://medium.com/@jean.dosher
import re
from collections import defaultdict
def refactor_yaml(yaml_content):
screen_name = get_screen_name(yaml_content)
if screen_name is None:
print("Unable to refactor: Screen name not found")
return yaml_content
lines = yaml_content.split('\n')
refactored_lines = []
control_counters = defaultdict(int) # for control name generation (if needed)
control_name_map = {} # to store old to new control name mapping
for i, line in enumerate(lines):
if re.search(r'\w+_\d+ As \w+(\.\w+)?:', line):
indentation = len(line) - len(line.lstrip())
control_name, control_type = line.strip().split(" As ")
control_type = control_type.split('.')[0] # Remove any subtype after the dot
# Only refactor if the control name ends with _\d+
if re.search(r'_\d+$', control_name) or '_Unknown' in control_name:
container_name = get_container_name(indentation, lines, i)
# Get control properties
control_props = {}
j = i + 1
while j < len(lines) and lines[j].startswith(' ' * (indentation + 4)):
stripped_line = lines[j].strip()
if ': ' in stripped_line:
prop, value = stripped_line.split(": ", 1)
control_props[prop] = value
j += 1
context_purpose = get_context_purpose(control_props)
new_name = refactor_control_name(control_name, control_type, context_purpose, container_name, screen_name, control_counters)
control_name_map[control_name] = new_name
refactored_lines.append(line.replace(control_name, new_name))
else:
refactored_lines.append(line)
else:
# Check for references to other controls in properties
for old_name, new_name in control_name_map.items():
if old_name in line:
line = re.sub(r'\b' + re.escape(old_name) + r'\b', new_name, line)
refactored_lines.append(line)
refactored_content = '\n'.join(refactored_lines)
return refactored_content
# Updated get_screen_name function
def get_screen_name(yaml_content):
# Pattern to match both quoted and unquoted screen names
pattern = r"(?:['\"](.*?)['\"]\s+As\s+screen|(\w+)\s+As\s+screen)"
match = re.search(pattern, yaml_content)
if match:
# The screen name will be in either group 1 or group 2
screen_name = match.group(1) or match.group(2)
print(f"Screen name found: {screen_name}")
return screen_name
else:
print("Screen name not found in YAML content")
return None
def get_container_name(indentation, lines, current_index):
for i in range(current_index - 1, -1, -1):
line = lines[i].strip()
if " As " in line and any(container_type in line for container_type in ["group", "groupContainer", "container"]):
return line.split(" As ")[0]
return "Unknown"
def get_context_purpose(control_props):
return control_props.get("Icon", control_props.get("Text", control_props.get("Tooltip", "Unknown")))
def refactor_control_name(old_name, control_type, context_purpose, container_name, screen_name, control_counters):
control_type_prefix = control_type.lower()[:3]
context = re.sub(r'[^a-zA-Z0-9]', '', context_purpose)[:10]
control_counters[control_type] += 1
return f"{control_type_prefix}_{context}_{container_name}_{screen_name}_{control_counters[control_type]}"
# Main execution
if __name__ == "__main__":
# Ask the user for the input filename
input_filename = input("Enter the name of the YAML file to refactor: ")
# Read the YAML file
try:
with open(input_filename, 'r', encoding='utf-8') as file:
yaml_content = file.read()
except FileNotFoundError:
print(f"Error: The file '{input_filename}' was not found.")
exit(1)
except IOError:
print(f"Error: Unable to read the file '{input_filename}'.")
exit(1)
# Refactor the YAML content
refactored_yaml = refactor_yaml(yaml_content)
# Write the refactored YAML to a new file
output_filename = f"refactored_{input_filename}"
with open(output_filename, 'w', encoding='utf-8') as file:
file.write(refactored_yaml)
print(f"Refactoring complete. Check '{output_filename}' for the result.")
🤓 Usage
For now, the usage is for mid-level users:
1. The user must follow the unpack/pack process mentioned in last article to be able to unpack the msapp (through CLI canvas pack/unpack) and copy the screen YAML file from Src directory

2. In application directory, paste the copied YAML file and run the script:

As output, we get refactored_{filename}.yaml
. Now just copy this into Src directory of unpacked APP, exclude the “old” file and rename the refactored file
3. Pack the APP with PAC canvas pack
4. I really don’t know if there is another way to do this, but to open the msapp in my environment (without importing as a zip) is opening any APP in Power Apps Studio and open the refactored msapp:

🚑 Bumps in the road
Now, i need to put some testimonials here about the process of developing this application.
As a concept I intended that the application to be user friendly, with a user interface, etc.…
So, I’ve used Cursor AI code editor to transcript the code to C# and wrap it in a bigger solution with Windows Forms with every beauty charming UI, but got stuck trying to wrap the pack/unpack process into solution.
I got even more excited knowing some amazing tools in Power Apps Tooling repo, one these tools just seem to fit perfectly into my problem, that is the Legacy Source File Pack and Unpack Utility (PASopa). I ended up ruining everything and decided to keep it simple for me and my team usage
🏁 Conclusion
Well, after all, I solved this huge problem that i have and maybe in the near future we can improve it to refactor some other cases.
For now, a container of a group is suggested for the naming and if there is absolutely no context the renaming still serves it proposes giving the minimal refactored name possible (the control type and screen name at the end). Even using LLM’s sometimes is impossible to get some context of the Control (trust me, I tried)
🐞Known Bugs:
– Screen Names with a space add a ‘ to the refactored controls, and this breaks when trying to pack to .msapp
Leave a Reply