Introduction
Let me just start off with a question: Why the hell isn't there some kind of UI designer for Neoforce? Can't we at least make some kind of app to parse the InitializeComponent method of a form class or an XAML file and extract and convert supported control types to a Neoforce layout file? At least that way we'd be able to use the designer in VS and run the files through said program to generate layout files. Sounds like a complicated undertaking...But I digress.
So you want to figure out those damn layout files, eh? Perhaps you were poking around in the Neoforce Central project code and stumbled upon line 256 in Logic.cs:
Window tmp = (Window)Layout.Load(Manager, "Window");
Wow, that's a hell of a lot better than the few hundred lines of code it took to initialize a window in the Layout.cs file of the same project. Go have a look at it if you want, it's pretty horrendous, and I'm not cluttering up this post with screens of code we don't even care about.
So, can all windows be loaded in one line? Well, not exactly. You'll still have to wire up events and initialize some controls, but you won't have to set up the properties for each control in code.
The Layout File
With the lack of documentation, it seems the best place to start looking for some clues is in the previously referenced "Window" asset. Open up that XML file and you see this:
<Layout Name="Window" Version="0.7">
<Info>
<Name>Window</Name>
<Description>Sample window layout.</Description>
<Author>Tom Shane</Author>
<Version>0.7</Version>
</Info>
<Controls>
<Control Name="frmMain" Class="Window">
<Properties>
<Property Name="Left" Value="300"/>
<Property Name="Top" Value="200"/>
<Property Name="MinimumWidth" Value="300"/>
</Properties>
<Controls>
<Control Name="btnOk" Class="Button">
<Properties>
<Property Name="Left" Value="100"/>
<Property Name="Top" Value="100"/>
<Property Name="Text" Value="btn!"/>
</Properties>
</Control>
</Controls>
</Control>
</Controls>
</Layout>
Aw hell, that doesn't look so bad. A simple file for a simple window. And, according to the Layout class, lines 2-7 aren't even supposed to be there. Seems the Info tag is more for Skin files. There isn't a whole wealth of information to be gained by looking at this sample file though, so I think we should go check out the parser and see what's going on there.
The Layout Class
The Layout class is responsible for parsing these layout files, so checking out the code here should resolve many of the mysteries surrounding the XML Layout files. Let me just clean up this source code and post it here as a reference:
namespace TomShane.Neoforce.Controls
{
public static class Layout
{
public static Container Load(Manager manager, string asset)
{
Container win = null;
LayoutXmlDocument doc = new LayoutXmlDocument();
ArchiveManager content = new ArchiveManager(manager.Game.Services);
try
{
content.RootDirectory = manager.LayoutDirectory;
#if (!XBOX && !XBOX_FAKE)
string file = content.RootDirectory + asset;
if (File.Exists(file))
{
doc.Load(file);
}
else
#endif
{
doc = content.Load<LayoutXmlDocument>(asset);
}
if (doc != null && doc["Layout"]["Controls"] != null && doc["Layout"]["Controls"].HasChildNodes)
{
XmlNode node = doc["Layout"]["Controls"].GetElementsByTagName("Control").Item(0);
string cls = node.Attributes["Class"].Value;
Type type = Type.GetType(cls);
if (type == null)
{
cls = "TomShane.Neoforce.Controls." + cls;
type = Type.GetType(cls);
}
win = (Container)LoadControl(manager, node, type, null);
}
}
finally
{
content.Dispose();
}
return win;
}
private static Control LoadControl(Manager manager, XmlNode node, Type type, Control parent)
{
Control c = null;
Object[] args = new Object[] {manager};
c = (Control)type.InvokeMember(null, BindingFlags.CreateInstance, null, null, args);
if (parent != null) c.Parent = parent;
c.Name = node.Attributes["Name"].Value;
if (node != null && node["Properties"] != null && node["Properties"].HasChildNodes)
{
LoadProperties(node["Properties"].GetElementsByTagName("Property"), c);
}
if (node != null && node["Controls"] != null && node["Controls"].HasChildNodes)
{
foreach (XmlElement e in node["Controls"].GetElementsByTagName("Control"))
{
string cls = e.Attributes["Class"].Value;
Type t = Type.GetType(cls);
if (t == null)
{
cls = "TomShane.Neoforce.Controls." + cls;
t = Type.GetType(cls);
}
LoadControl(manager, e, t, c);
}
}
return c;
}
private static void LoadProperties(XmlNodeList node, Control c)
{
foreach (XmlElement e in node)
{
string name = e.Attributes["Name"].Value;
string val = e.Attributes["Value"].Value;
PropertyInfo i = c.GetType().GetProperty(name);
if (i != null)
{
{
try
{
i.SetValue(c, Convert.ChangeType(val, i.PropertyType, null), null);
}
catch
{
}
}
}
}
}
}
}
The first thing to notice is the fact that Layout.Load does NOT return a Window. This leads me to believe that you can use any Neoforce container control as the root control in a layout file. (Console, GroupBox, GroupPanel, Panel, SideBarPanel, StackPanel, TabControl, Window, and any derivatives) That's cool.
Next you'll see that 2nd if statement in Layout.Load. Considering it's looking for ["Layout"]["Controls"].HasChildNodes, we can assume that Layout files require a Layout tag, and a Controls tag with at least one Control tag as a child. Given our previous discovery, we can assume that the 1st Control tag must be a container type.
Inside the if statement we just discussed, it shows that a Control tag must have a Class attribute and following the code into Layout.LoadControl, we can see that the Name attribute is also required. Though you can leave the string empty if you'll never need to access the control from code.
The Properties tag in a control is optional if you don't need to set properties, or if you plan on setting them in code, however, if the Properties tag is there, it must have at least one Property child tag. Each Property tag is required to have a Name and Value attribute. The Name attribute's value can be any public property that the class defines.
Each Control tag can have a number of children specified in its own local Controls tag. An assumed requirement is that any Control with child controls is, itself, is derived from the Container class.
Sample Layout Structure
Applying what we have learned, it appears that the most basic Layout file will be structured as follows:
<Layout>
<Controls>
<!--Root control: MUST be a type derived from Container.-->
<Control Name="" Class="ClassDerivedFromContainer">
<!--Optional, but you'll probably want to set a few values here in most cases-->
<Properties>
<Property Name="AnyPublicPropertyOrField" Value="SomeNumberOrString"/>
</Properties>
<!--Optional in most cases, but, you do want child controls in the root container right?-->
<Controls>
<!--You can insert more Control tags here if needed.-->
</Controls>
</Control>
</Controls>
</Layout>
And there you have it. I'm going to attempt to get a sample project put together by the end of the weekend. But, until then, I hope you found this information useful.