{"record":{"id":"64c86270ad499fd2","repo":"Textualize/textual","slug":"the-message-class-must-have-a-control-to-match-w","errorCode":null,"errorMessage":"The message class must have a 'control' to match with the on decorator","messagePattern":"The message class must have a 'control' to match with the on decorator","errorType":"exception","errorClass":"OnDecoratorError","httpStatus":null,"severity":"error","filePath":"src/textual/_on.py","lineNumber":69,"sourceCode":"\n    Args:\n        message_type: The message type (i.e. the class).\n        selector: An optional [selector](/guide/CSS#selectors). If supplied, the handler will only be called if `selector`\n            matches the widget from the `control` attribute of the message.\n        **kwargs: Additional selectors for other attributes of the message.\n    \"\"\"\n\n    selectors: dict[str, str] = {}\n    if selector is not None:\n        selectors[\"control\"] = selector\n    if kwargs:\n        selectors.update(kwargs)\n\n    parsed_selectors: dict[str, tuple[SelectorSet, ...]] = {}\n    for attribute, css_selector in selectors.items():\n        if attribute == \"control\":\n            if message_type.control == Message.control:\n                raise OnDecoratorError(\n                    \"The message class must have a 'control' to match with the on decorator\"\n                )\n        elif attribute not in message_type.ALLOW_SELECTOR_MATCH:\n            raise OnDecoratorError(\n                f\"The attribute {attribute!r} can't be matched; have you added it to \"\n                + f\"{message_type.__name__}.ALLOW_SELECTOR_MATCH?\"\n            )\n        try:\n            parsed_selectors[attribute] = parse_selectors(css_selector)\n        except TokenError:\n            raise OnDecoratorError(\n                f\"Unable to parse selector {css_selector!r} for {attribute}; check for syntax errors\"\n            ) from None\n\n    def decorator(method: DecoratedType) -> DecoratedType:\n        \"\"\"Store message and selector in function attribute, return callable unaltered.\"\"\"\n\n        if not hasattr(method, \"_textual_on\"):","sourceCodeStart":51,"sourceCodeEnd":87,"githubUrl":"https://github.com/Textualize/textual/blob/06dbeef4bb70fb718236aa418ed658ef4667a126/src/textual/_on.py#L51-L87","documentation":"The @on decorator lets you bind handlers to message attributes via CSS-like selectors, e.g. @on(Input.Submitted, \"#search\"). The special pseudo-selector \"control\" (e.g. @on(Button.Pressed, \"save\")) is only legal when the message class overrides the inherited Message.control property. If the message type still uses the base Message.control, Textual cannot resolve what 'control' the selector refers to, so it raises OnDecoratorError at decoration time.","triggerScenarios":"Writing @on(MyMessage, \"something\") where MyMessage subclasses Message (directly or via a base that never defines control) and therefore has no control property override; using @on(Button.Pressed, \"save\") style control selectors with a custom message type that forgot to implement control; decorating a method on a custom message class that was copied without the control property.","commonSituations":"Defining custom messages for custom widgets and assuming control matching works like it does for Button.Pressed/Input.Submitted; upgrading apps where a message hierarchy changed to no longer inherit a control override; matching on a Select/Tab-like message whose library class does define control but a user subclass shadows it away.","solutions":["Add a control property override to your message class returning the widget the message is about","Use a different supported attribute from the message's ALLOW_SELECTOR_MATCH (or none) in the decorator","If you meant widget-id matching, ensure you are using a message type like Button.Pressed that implements control"],"exampleFix":"# before\nclass Saved(Message):\n    pass\n\n@on(Saved, \"save\")  # OnDecoratorError\ndef handle(self, event: Saved) -> None: ...\n\n# after\nclass Saved(Message):\n    def __init__(self, button: Button) -> None:\n        super().__init__(button)\n\n    @property\n    def control(self) -> Button:\n        return self._sender if isinstance(self._sender, Button) else None  # or stored ref","handlingStrategy":"type-guard","validationCode":"from textual.message import Message\n\ndef supports_control(msg_type: type[Message]) -> bool:\n    return msg_type.control is not Message.control","typeGuard":"def has_control_override(msg_type: type) -> TypeGuard[type]:\n    from textual.message import Message\n    return getattr(msg_type, \"control\", None) is not Message.control","tryCatchPattern":"raise OnDecoratorError only at import; guard by asserting supports_control(MyMsg) in unit tests before shipping decorators","preventionTips":["When creating custom messages with control matching, always implement the control property","Unit-test module import so decorator errors fail fast in CI","Copy the message-class pattern from Button.Pressed when adding control"],"tags":["textual","decorator","event-handling","selector"],"backgroundTag":"decorator-argument-mismatch","analyzedSha":"06dbeef4bb70fb718236aa418ed658ef4667a126","analyzedAt":"2026-08-27T02:36:57.214Z","schemaVersion":2},"datasetVersion":"2026-08-27T03:17:27.898Z"}