Changing the appearance of M3 list cells with conditional styles is easy but in some cases you need to do things that require logic that cannot be expressed with conditional styles alone. This post will show you how to change the style of list cells in M3 lists using JScript, custom data templates and custom data template selectors.
Background
The MForms list uses data templates for displaying the text in all cells. Data templates are very common in WPF applications and they are used for customization of data presentation. If you are not familiar with data templates I would recommend that you read about them before you start to modify the examples in this post. There is an overview at MSDN here: Data Templating Overview
Data templates are very powerful but they also add complexity when you want to do something basic, like changing the color of the text in a list cell. To keep the examples simple I will only use one template that changes the foreground color for the text in list cells. The focus of this post is not about how to create complex data templates but rather how to use data templates from JScript in MForms. There are plenty of information about how to write WPF data templates in XAML on the web.
Custom data templates
When creating data templates in a regular WPF application or a Smart Office SDK feature you would write them in a XAML file. Using XAML files in JScript is not an option so we need another approach. The actual template markup will still be XAML but defined in a string in the JScript. The XAML string can then be parsed to a template instance using the WPF XamlReader class. To make the template XAML a bit more flexible and reusable we can add placeholders and the String.Format method to replace them with actual values.
The data template that will be used in the examples is defined like this:
var xamlTemplate = "<DataTemplate><TextBlock Text=\"{{Binding Path={0}, Mode=OneWay}}\" Foreground=\"{1}\" /></DataTemplate>";
The template contains a single TextBlock element that binds the value of the Text property and defines a new text color using Foreground property. To make this template usable we need to replace the placeholders.
var xaml = String.Format(xamlTemplate, "[0]", "#FFCC0000");
This example would set a red foreground color for the cells in the first list column when applied to the list. This is a basic example and you could create very complex templates with custom content other than text for example.
Custom data template script example
The first script example is called CustomCellTemplateExample and shows you how to set one template for an entire list column. The second example will show how to use different templates in one column.
In the bottom of the script example there are two utility functions that can be reused without change in your own scripts. The ParseTemplate function is used in both examples for parsing a XAML string to a data template instance. The SetColumnTemplate function changes the column template for a grid view column at a specified index.
The CreateForegroundCellTemplate function contains the XAML template string and the code that replaces the placeholder with the values for the binding path and the foreground color.
Finally we have the Init function that uses the Dispatcher to delay the call to the ChangeTemplate function that defines the column index and the color to use for the data template. Note that the list view items are refreshed to ensure that the template is applied.
The screenshot below shows the script running in CRS055. All cells in the first list column now have the red foreground color defined in the template.
import System; import System.Windows; import System.Windows.Controls; import System.Windows.Data; import System.Windows.Media; import System.Windows.Markup; import System.Windows.Threading; import MForms; package MForms.JScript { public class CustomCellTemplateExample { var controller; public function Init(element : Object, args : Object, controller : Object, debug : Object) { this.controller = controller; var action : Action = ChangeTemplate; Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Input, action); } private function ChangeTemplate() { var columnIndex = 0; var bindingPath = "[" + columnIndex + "]"; var brush = "#FFCC0000"; var template = CreateForegroundCellTemplate(bindingPath, brush); var listView = controller.RenderEngine.ListControl.ListView; SetColumnTemplate(listView, columnIndex, template); listView.Items.Refresh(); } private function CreateForegroundCellTemplate(bindingPath, brush) { var xamlTemplate = "<DataTemplate><TextBlock Text=\"{{Binding Path={0}, Mode=OneWay}}\" Foreground=\"{1}\" /></DataTemplate>"; var xaml = String.Format(xamlTemplate, bindingPath, brush); return ParseTemplate(xaml); } private function SetColumnTemplate(listView, columnIndex, template) { var gridView = listView.View; var column = gridView.Columns[columnIndex]; column.CellTemplateSelector = null; column.CellTemplate = null; column.CellTemplate = template; } private function ParseTemplate(xaml) { var context = new ParserContext(); context.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation"); context.XmlnsDictionary.Add("x", "http://schemas.microsoft.com/winfx/2006/xaml"); return XamlReader.Parse(xaml, context); } } }
Custom data template selectors
In many cases a single data template for a list column is too limiting. You might want to apply several different templates depending on the value in the list cells. This can be solved by extending the WPF DataTemplateSelector class. Your custom date template selector will override the SelectTemplate method where you can choose which template to return depending on the data for the current list row.
Custom data template selector example
The second script example is called CustomCellTemplateSelectorExample and shows you how to use a data template selector that can return two different templates. This example shares some of the functions in the previous example so I’ll just cover the ones that are unique to this example.
At the end of the example you will find the CustomTemplateSelector class that extends the DataTemplateSelector class. Make sure to place additional classes such as this one after the main script class. The CustomTemplateSelector class has properties for the column index it applies to and for the two different templates it can return. The implementation of the SelectTemplate method gets the text for the cell by using the indexer on the item parameter. For an M3 list the item parameter will be an instance of the ListRow class that you might be familiar with. In a normal list the object returned from the list row indexer is a string. In editable list the object returned will be an EditableCell instance instead. If the cell text contains an “A” character a template with a green foreground color is returned, in all other cases a template with a red foreground color is returned.
The implementation of the SelectTemplate function can be a lot more complex than this simple example. You could compare values in different list cells on the same list row for example. The selector instance could also be initialized with more values that controls what the SelectTemplate function returns.
The remaining code is similar to the previous example but the SetColumnTemplateSelector function is used instead of the SetColumnTemplate function to set the template selector on the grid view column.
The screenshot below shows the script running in CRS055. The list cells in the first column that contains an “A” are green and the other are red.
import System; import System.Windows; import System.Windows.Controls; import System.Windows.Data; import System.Windows.Media; import System.Windows.Markup; import System.Windows.Threading; import MForms; package MForms.JScript { public class CustomCellTemplateSelectorExample { var controller; var debug; public function Init(element : Object, args : Object, controller : Object, debug : Object) { this.controller = controller; this.debug = debug; var action : Action = ChangeTemplate; Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Input, action); } private function ChangeTemplate() { var columnIndex = 0; var bindingPath = "[" + columnIndex + "]"; var brush; var listView = controller.RenderEngine.ListControl.ListView; var selector = new CustomTemplateSelector(); selector.columnIndex = 0; selector.templateRed = CreateForegroundCellTemplate(bindingPath, "#FFCC0000"); selector.templateGreen = CreateForegroundCellTemplate(bindingPath, "#FF00CC00"); SetColumnTemplateSelector(listView, columnIndex, selector); listView.Items.Refresh(); } private function CreateForegroundCellTemplate(bindingPath, brush) { var xamlTemplate = "<DataTemplate><TextBlock Text=\"{{Binding Path={0}, Mode=OneWay}}\" Foreground=\"{1}\" /></DataTemplate>"; var xaml = String.Format(xamlTemplate, bindingPath, brush); return ParseTemplate(xaml); } private function SetColumnTemplateSelector(listView, columnIndex, selector) { var gridView = listView.View; var column = gridView.Columns[columnIndex]; column.CellTemplateSelector = selector; } private function ParseTemplate(xaml) { var context = new ParserContext(); context.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation"); context.XmlnsDictionary.Add("x", "http://schemas.microsoft.com/winfx/2006/xaml"); return XamlReader.Parse(xaml, context); } } public class CustomTemplateSelector extends DataTemplateSelector{ public var columnIndex; public var templateRed; public var templateGreen; public override function SelectTemplate(item : Object, container : DependencyObject) : DataTemplate{ var text = item[columnIndex]; if (text.Contains("A")) { return templateGreen; } else { return templateRed; } } } }
Final notes
When creating your own templates I would suggest that you test them in a WPF test application or perhaps in the Mashup Designer before using them in a script. You want to be sure that your XAML code is correct before adding the additional complexity of using it from a script. Incorrect templates can cause quite severe errors in WPF in some cases.
If you use a lot of templates or complex templates you might consider caching the parsed template instances. This could be done using either the SessionCache or the InstanceCache to avoid frequent parsing of the data templates.
Great! I have been needing this. Thank you.
Another tip if you need to set different templates based on a value in the row before – for example for grouping by color.
In the SelectTemplate method you can use the name property on the item to get the Row name.
var rowName = item.Name; // r1, r2 etc. To get the row index substring the name to get the number eg, r3->3-1=2-> current row index.
Then do this to get the row before: var previousRow = listView.Items[currentRowIndex-1] (don’t forget checks if you are on the first row).
This tip is really interesting. Thanks for posting it.
How is it possible to do the same thing for an editable cell. I have trying to do this for quite some time without finding anything relevant. What should be the xaml template for this kind of cell? Is it the only and right way to do it?
Thanks in advance for any help on this!