Generate Proxies for the WPF frontend

In the previous post we learned how a WPF user interface can be driven from a java application. The solution that was presented allows showing views, modifying controls’ properties, and starting or stopping animations. It is based on a set of generic functions that provide for the required functionality. Each one of these takes additional parameters to adress a specific view, control, animation, and property.

Assuming there is a control named mute in the view main. To set its boolean property checked to true, we need to call wpfFrontend.setWidgetPropertyBool("main", "mute", "checked", true). This call will fail, when the view, the control, or the property is not found for the given names, which can likely happen because of mispelling or after refactoring the user interface. Such errors cannot be detected by the Java compiler and will lead to runtime errors that are hard to detect. One can use proxies that encapsulate all the adressing in order to avoid these problems and offer a more convenient way to access the elements of the WPF frontend. Using proxies the modification of the control’s property can be done by a call to a setter, e.g. wpfFrontend.main.mute.setChecked(true).

This post shows how such Java proxies can be generated from a high-level description of the frontend elements.

Role of the generated proxies

The following snippet shows an example of such a frontend description:

controls { // introduce the control types and their properties
	Label {
		string caption;
		integer x;
		integer y;
	}
	Slider {
		double value;
	}
	CheckBox {
		boolean checked;
	}
}

animations { // define the available animations
	startupAnimation,
	shutdownAnimation
}

views { // define the available views and their controls
	startup {
		Label percentageLoaded;
	}
	main {
		Label trackTitle;
		Label artist;
		Label discName;
	}
	setup {
		CheckBox muted;
		Slider volumeSlider;
		Label volumeLabel;
	}
}

Create the DSL for the WPF Frontend Description

In Xtext such a DSL is defined by the following grammar:

grammar org.xtext.example.wpfwrapper.WpfProxyDsl with org.eclipse.xtext.common.Terminals

generate wpfProxyDsl "http://www.xtext.org/example/wpfwrapper/WpfProxyDsl"

Main: {WpfProxy}
	('controls' '{' (widgetDeclarations+=WidgetDeclaration)+ '}' )?
	('animations' '{' (animations+=Animation (',' animations+=Animation)*)? '}' )?
	('views' '{' (views+=View)* '}' )?;

WidgetDeclaration:
	name=ID '{'
		(properties+=WidgetPropertyDeclaration)+
	'}';

WidgetPropertyDeclaration:
	datatype=Datatype name=ID ';';

enum Datatype:
	STRING="string" |
	INTEGER="integer" |
	DOUBLE="double" |
	BOOLEAN="boolean";

Animation:
	name=ID;
View:
	name=ID '{' (widgets+=Widget)* '}';
Widget:
	declaration=[WidgetDeclaration] name=ID ';';

Now lets Xtext’s generator do the job and create the tooling for this DSL.

Framework

A powerful framework helps to minimize the amount of generated code. The class AppWrapperProxy is the abstract base class for all proxies. It holds an instance of the java server that is used by the proxies to communicate with the WPF frontend.

public abstract class AppWrapperProxy {
	public AppWrapperProxy(WPFAppWrapper appWrapper) {
		this.appWrapper = appWrapper;
	}

	private WPFAppWrapper appWrapper;

	WPFAppWrapper getAppWrapper() {
		return appWrapper;
	}
}

We will generate proxies for animations, views and controls. For each of these proxy types we will provide another base class that holds the element’s name and provides some basic functions:

public class AnimationProxy extends AppWrapperProxy {
	public AnimationProxy(WPFAppWrapper appWrapper, String animationName) {
		super(appWrapper);
		this.animationName = animationName;
	}
	private String animationName;

	public void start() {
		getAppWrapper().startAnimation(animationName);
	}
	public void stop() {
		getAppWrapper().stopAnimation(animationName);
	}
}

public abstract class ViewProxy extends AppWrapperProxy {
	public ViewProxy(WPFAppWrapper appWrapper, String viewName) {
		super(appWrapper);
		this.viewName = viewName;
	}
	private String viewName;

	String getViewName() {
		return viewName;
	}

	public void show() {
		getAppWrapper().showView(viewName);
	}
}

public abstract class WidgetProxy extends AppWrapperProxy {
	public WidgetProxy(WPFAppWrapper appWrapper, ViewProxy view, String widgetName) {
		super(appWrapper);
		this.view = view;
		this.widgetName = widgetName;
	}
	private ViewProxy view;
	private String widgetName;

	ViewProxy getView() {
		return view;
	}
	String getWidgetName() {
		return widgetName;
	}
}

Read Source Files and invoke Code Generation

The next step is to build a code generator that creates the proxies from the description of the frontend elements. The code generation is controlled by a MWE workflow. It reads the source models, cleans the output folder and then runs code generation.

module workflow.HmiWrapperGenerator

import org.eclipse.emf.mwe.utils.*

var targetDir = "src-gen"
var fileEncoding = "Cp1252"
var modelPath = "src/model"

Workflow {
	component = org.eclipse.xtext.mwe.Reader {
		path = modelPath
		register =
			org.xtext.example.wpfwrapper.WpfProxyDslStandaloneSetup {}
		load = {
			slot = "model"
			type = "WpfProxy"
		}
	}

	component = org.eclipse.emf.mwe.utils.DirectoryCleaner {
		directory = targetDir
	}

	component = org.eclipse.xpand2.Generator {
		metaModel = org.eclipse.xtend.type.impl.java.JavaBeansMetaModel {}
		metaModel = org.eclipse.xtend.typesystem.emf.EmfMetaModel {
			metaModelPackage =
				"org.xtext.example.wpfwrapper.wpfProxyDsl.WpfProxyDslPackage"
		}
		expand = "templates::Template::main FOREACH model"
		outlet = {
			path = targetDir
			postprocessor = org.eclipse.xpand2.output.JavaBeautifier {}
		}
		fileEncoding = fileEncoding
	}
}

The workflow uses the regular reader component to parse the source file and provide a model in memory for code generation. This requires to provide a name for the root element of our DSL. Therefore we need to register this name provider in the RuntimeModule:

public class WpfProxyNameProvider extends
	DefaultDeclarativeQualifiedNameProvider {

	public String qualifiedName(WpfProxy wpfProxy) {
		return wpfProxy.eResource().getURI().toString();
	}
}

Code Generator

The final step is to create the code generator by means of Xpand and Xtend.

This is the main template:

«IMPORT org::xtext::example::wpfwrapper::wpfProxyDsl»

«EXTENSION templates::Extensions»

«DEFINE main FOR WpfProxy-»
	«EXPAND Widget FOREACH widgetDeclarations-»
	«EXPAND View FOREACH views-»
	«EXPAND Animations FOR this-»
	«EXPAND Views FOR this-»
«ENDDEFINE»

«DEFINE Views FOR WpfProxy»
	«FILE "org/eclipse/xtext/hmiwrapper/Views.java"-»
		package org.eclipse.xtext.hmiwrapper;

		public class Views {
			public Views(WPFAppWrapper appWrapper) {
				«FOREACH views AS view-»
					«view.name» = new «view.name.toFirstUpper()-»View(appWrapper, "«view.name-»");
				«ENDFOREACH-»
			}

			«FOREACH views AS view-»
				public «view.name.toFirstUpper()-»View «view.name-»;
			«ENDFOREACH-»
		}
	«ENDFILE-»
«ENDDEFINE»

«DEFINE View FOR View»
	«FILE "org/eclipse/xtext/hmiwrapper/" + name.toFirstUpper() + "View.java"-»
		package org.eclipse.xtext.hmiwrapper;

		public class «name.toFirstUpper()-»View extends ViewProxy {
			public «name.toFirstUpper()-»View(WPFAppWrapper appWrapper, String viewName) {
				super(appWrapper, viewName);
				«FOREACH widgets AS widget-»
					«widget.name-» = new «widget.declaration.name.toFirstUpper()-»Widget(appWrapper, this, "«widget.name-»");
				«ENDFOREACH-»
			}

			«FOREACH widgets AS widget-»
				public «widget.declaration.name.toFirstUpper()-»Widget «widget.name-»;
			«ENDFOREACH-»
		}
	«ENDFILE-»
«ENDDEFINE»

«DEFINE Animations FOR WpfProxy»
	«FILE "org/eclipse/xtext/hmiwrapper/Animations.java"-»
		package org.eclipse.xtext.hmiwrapper;

		public class Animations {
			public Animations(WPFAppWrapper appWrapper) {
				«FOREACH animations AS animation-»
					«animation.name-» = new AnimationProxy(appWrapper, "«animation.name-»");
				«ENDFOREACH-»
			}

			«FOREACH animations AS animation-»
				public AnimationProxy «animation.name-»;
			«ENDFOREACH-»
		}
	«ENDFILE-»
«ENDDEFINE»

«DEFINE Widget FOR WidgetDeclaration»
	«FILE "org/eclipse/xtext/hmiwrapper/" + name.toFirstUpper()+"Widget.java"-»
		package org.eclipse.xtext.hmiwrapper;

		public class «name.toFirstUpper()-»Widget extends WidgetProxy {
			public «name.toFirstUpper()-»Widget(WPFAppWrapper appWrapper, ViewProxy view, String widgetName) {
				super(appWrapper, view, widgetName);
			}

			«FOREACH properties AS property-»
				public void set«property.name.toFirstUpper()-»(«property.datatype.toJavaType()-» newValue) {
					getAppWrapper().«property.datatype.toWidgetPropertySetter()-»(getView().getViewName(), getWidgetName(), "«property.name-»", newValue);
				}
			«ENDFOREACH-»
		}
	«ENDFILE-»
«ENDDEFINE»

This template uses two helper function that are provided in a Xtend file:

import org::xtext::example::wpfwrapper::wpfProxyDsl;

String toWidgetPropertySetter(Datatype datatype):
	if (datatype == Datatype::STRING) then (
		"setWidgetPropertyString"
	) else (
	if (datatype == Datatype::INTEGER) then (
		"setWidgetPropertyInt"
	) else (
	if (datatype == Datatype::DOUBLE) then (
		"setWidgetPropertyDouble"
	) else (
	if (datatype == Datatype::BOOLEAN) then (
		"setWidgetPropertyBool"
	) else (
		null
	))));

String toJavaType(Datatype datatype):
	if (datatype == Datatype::STRING) then (
		"String"
	) else (
	if (datatype == Datatype::INTEGER) then (
		"int"
	) else (
	if (datatype == Datatype::DOUBLE) then (
		"double"
	) else (
	if (datatype == Datatype::BOOLEAN) then (
		"boolean"
	) else (
		null
	))));

Usage Example

Assuming the object animation is an instance of the generated class Animations and views is an instance of the generated class Views. The following calls are valid and result in the corresponding modifications of the WPF frontend elements:

  • animations.shutdownAnimation.stop()
  • views.main.show()
  • views.setup.muted.setChecked(false)
  • views.main.artist.setX(100)

Conclusion

The previous post described how a remote WPF frontend can be manipulated from a Java backend. However, its generic programming interface leads to programming errors due to wrong adressing of the frontend elements. This posts describes how this drawbacks can be avoided by generating proxies. These proxies allow to manipulate the remote frontend elements like native Java UI elements.

By writing another socket client this solution can also be applied to other user interface technologies. Thus it can also serve for abstracting from user interface technologies. This enables to use the same backend application with different frontends implemented build with various user interface technologies.

The source code of this post is available here.

Advertisements
  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: