Someone (impf) over at the Media Center Sandbox asked this, and since it's been asked several times before I figured I'd do a proper writeup rather than just a quick reply in the forums.

First, to make the control fully re-usable, it should rely on as few properties as possible. Because of this, my Spinner requires nothing but a single Choice object - it allows for many other properties to be passed, but the Choice is the only mandatory one.
One of the criteria impf set was that he should be able to create the Choice in his C# class, and have it updated as and when the spinner is 'spun'. So, let's get to it.
I'll split this into parts, were we'll start with the smaller elements first. So to kickstart things, here's the code needed for the Plus and Minus buttons;
<UI Name="SpinnerButton">
<Properties>
<Command Name="Command"/>
<Image Name="NoFocusImage" Image="$Required"/>
<Image Name="FocusImage" Image="$Required"/>
<Image Name="DisabledImage" Image="$Required"/>
<Image Name="FocusOverlayImage" Image="$Required"/>
<Image Name="PressedHighlightImage" Image="$Required"/>
<Size Name="Size" Size="51,51"/>
<cor:Boolean Name="Dormant" Boolean="false"/>
<Sound Name="ClickSound" Sound="$Required"/>
<Sound Name="FocusSound" Sound="$Required"/>
</Properties>
<Locals>
<ClickHandler Name="ClickHandler"/>
</Locals>
<Rules>
<Binding Source="[Command.Available]" Target="[Input.Enabled]"/>
<Condition Source="[Input.Enabled]" SourceValue="false">
<Actions>
<Set Target="[Background.Content]" Value="[DisabledImage]" />
</Actions>
</Condition>
<Changed Source="[ClickHandler.Invoked]">
<Actions>
<PlaySound Sound="[ClickSound]"/>
<Invoke Target="[Command.Invoke]"/>
</Actions>
</Changed>
<Condition Source="[Input.KeyFocus]" ConditionOp="Equals" SourceValue="true">
<Actions>
<PlaySound Sound="[FocusSound]"/>
<Set Target="[Background.Content]" Value="[FocusImage]"/>
</Actions>
</Condition>
<Condition Source="[ClickHandler.Clicking]" SourceValue="true">
<Actions>
<Set Target="[PressedHighlight.Visible]" Value="true"/>
</Actions>
</Condition>
<Binding Source="[Input.KeyFocus]" Target="[FocusOverlay.Visible]"/>
</Rules>
<Content>
<Panel>
<Children>
<Clip Layout="Scale" FadeSize="10">
<Children>
<Graphic Name="PressedHighlight" Content="[PressedHighlightImage]" Visible="false" MaximumSize="[Size]">
<Animations>
REMOVED FOR READABILITY
</Animations>
</Graphic>
</Children>
</Clip>
<Graphic Name="FocusOverlay" Layout="Fill" Content="[FocusOverlayImage]" MaximumSize="[Size]">
<Animations>
REMOVED
</Animations>
</Graphic>
<Graphic Name="Background" Content="[NoFocusImage]" MaximumSize="[Size]"/>
</Children>
</Panel>
</Content>
</UI>
But hold on a minute there cowboy, didn't you say all that was needed was a single Choice? Ah yes, but that's for external usage - internal use doesn't matter as it will never change depending on your application. Bear with me and you'll see what I mean.
I'm assuming you as a reader is somewhat familiar with MCML by now, so I'm not going to dissect the code word by word. The only interesting thing we're really doing above is <Binding Source="[Command.Available]" Target="[Input.Enabled]"/>. This single line of code disables the button if there are no further items in the Choice (well, ok, there's more to it than that, but we'll get to that further "up" the control).
<UI Name="SpinnerTextBox">
<Properties>
<cor:String Name="Text" String="$Required"/>
<Size Name="Size" Size="251,51"/>
<cor:Boolean Name="Dormant" Boolean="false"/>
<Font Name="Font" Font="$Required"/>
<Color Name="Color" Color="color://comm:LightBlue"/>
<Image Name="NoFocusImage" Image="$Required"/>
<Image Name="FocusImage" Image="$Required"/>
<Image Name="DormantImage" Image="$Required"/>
</Properties>
<Rules>
<Binding Source="[Text]" Target="[Label.Content]"/>
</Rules>
<Content>
<Graphic Content="[FocusImage]" MaximumSize="[Size]" Padding="8,6,8,8" Margins="0,0,10,0">
<Children>
<Text Name="Label" Color="[Color]" Font="[Font]">
<Animations>
REMOVED
</Animations>
</Text>
</Children>
</Graphic>
</Content>
</UI>
Right. So again, we're not doing anything extraordinary. Simply taking in some properties and displaying them on the screen. Binding the property Text to the Text Element.
Ok, so let's move on to where everything comes together.
<UI Name="TextSpinner" BaseUI="me:Spinner">
<Rules>
<Binding Source="[Choice.Chosen!cor:String]" Target="[SpinnerText.Text]"/>
</Rules>
<Content>
<Panel Layout="VerticalFlow">
<Children>
<Text Name="LabelText" Font="[SpinnerLabelFont]" Color="[SpinnerColorNoFocus]" Visible="[ShowLabel]"/>
<Panel Layout="HorizontalFlow">
<Children>
<me:SpinnerTextBox Name="SpinnerText" Text="" Font="[SpinnerChoiceFont]" Color="[SpinnerColorFocus]" Size="[Size]" NoFocusImage="[SpinnerBackgroundNoFocus]" FocusImage="[SpinnerBackgroundFocus]" DormantImage="[SpinnerBackgroundDormant]"/>
<me:SpinnerButton Command="[Next]" NoFocusImage="[SpinnerPlusNoFocus]" FocusImage="[SpinnerPlusFocus]" DisabledImage="[SpinnerPlusDisabled]" FocusOverlayImage="[SpinnerPlusFocusOverlay]" PressedHighlightImage="[SpinnerPlusPressedHighlight]" ClickSound="[ClickSound]" FocusSound="[FocusSound]"/>
<me:SpinnerButton Command="[Previous]" NoFocusImage="[SpinnerMinusNoFocus]" FocusImage="[SpinnerMinusFocus]" DisabledImage="[SpinnerMinusDisabled]" FocusOverlayImage="[SpinnerMinusFocusOverlay]" PressedHighlightImage="[SpinnerMinusPressedHighlight]" ClickSound="[ClickSound]" FocusSound="[FocusSound]"/>
</Children>
</Panel>
</Children>
</Panel>
</Content>
</UI>
So that was my actual TextSpinner control. Doesn't look like much does it? The only thing to note is the use of <UI Name="TextSpinner" BaseUI="me:Spinner"> - this means TextSpinner inherits from another UI, namely Spinner. Which looks like this;
<UI Name="Spinner">
<Properties>
<Choice Name="Choice" Choice="$Required"/>
<Size Name="Size" Size="251,51"/>
<Font Name="SpinnerLabelFont" Font="font://comm:DialogContent"/>
<Font Name="SpinnerChoiceFont" Font="font://comm:ButtonText"/>
<cor:Boolean Name="ShowLabel" Boolean="true"/>
<Color Name="SpinnerColorNoFocus" Color="color://comm:LightBlue"/>
<Color Name="SpinnerColorFocus" Color="color://comm:OffWhite"/>
<Image Name="SpinnerPlusNoFocus" Image="image://comm:Spinner.Plus.NoFocus"/>
<Image Name="SpinnerPlusFocus" Image="image://comm:Spinner.Plus.Focus"/>
<Image Name="SpinnerPlusDisabled" Image="image://comm:Spinner.Plus.Disabled"/>
<Image Name="SpinnerPlusFocusOverlay" Image="image://comm:Spinner.Plus.FocusOverlay"/>
<Image Name="SpinnerPlusPressedHighlight" Image="image://comm:Spinner.Plus.PressedHighlight"/>
<Image Name="SpinnerMinusNoFocus" Image="image://comm:Spinner.Minus.NoFocus"/>
<Image Name="SpinnerMinusFocus" Image="image://comm:Spinner.Minus.Focus"/>
<Image Name="SpinnerMinusDisabled" Image="image://comm:Spinner.Minus.Disabled"/>
<Image Name="SpinnerMinusFocusOverlay" Image="image://comm:Spinner.Minus.FocusOverlay"/>
<Image Name="SpinnerMinusPressedHighlight" Image="image://comm:Spinner.Minus.PressedHighlight"/>
<Image Name="SpinnerBackgroundFocus" Image="image://comm:Spinner.Background.Focus"/>
<Image Name="SpinnerBackgroundNoFocus" Image="image://comm:Spinner.Background.NoFocus"/>
<Image Name="SpinnerBackgroundDormant" Image="image://comm:Spinner.Background.Dormant"/>
<Sound Name="ClickSound" Sound="sound://comm:Select.Mini"/>
<Sound Name="FocusSound" Sound="sound://comm:Focus"/>
</Properties>
<Locals>
<Command Name="Previous"/>
<Command Name="Next"/>
</Locals>
<Rules>
<Binding Source="[Choice.HasPreviousValue]" Target="[Previous.Available]"/>
<Binding Source="[Choice.HasNextValue]" Target="[Next.Available]"/>
<Binding Source="[Choice.Description]" Target="[LabelText.Content]"/>
<Changed Source="[Previous.Invoked]">
<Actions>
<Invoke Target="[Choice.PreviousValue]"/>
</Actions>
</Changed>
<Changed Source="[Next.Invoked]">
<Actions>
<Invoke Target="[Choice.NextValue]"/>
</Actions>
</Changed>
<Condition Source="[Input.DeepKeyFocus]" SourceValue="true">
<Actions>
<Set Target="[LabelText.Color]" Value="[SpinnerColorFocus]"/>
</Actions>
</Condition>
</Rules>
<Content>
<Panel Layout="VerticalFlow">
<Children
<Text Name="LabelText" Font="[SpinnerLabelFont]" Color="[SpinnerColorNoFocus]" Visible="[ShowLabel]"/>
</Children>
</Panel>
</Content>
</UI>
As you can see, this is where everything is actually glued together. I'll explain some of this in more detail, and other parts I'll leave to your imagination.
First of all, the content specified in Spinner will never be displayed because it is overridden by TextSpinner. Now, it's apparently considered bad practice to do this, but I've never had a problem with it, and seems to work wonders here. The only reason there is any content in Spinner is so the Rules will work properly. So why didn't I put everything from Spinner and put it into TextSpinner, making it a single UI you ask? Simple, I also have a ImageSpinner UI and a few others, which inherit in the same way from Spinner. So rather than update umpteen UIs if I want to change something in all my different Spinners, I can (in most cases) just change Spinner.
Right. As you can see, the only $Required property in Spinner is a Choice, everything else is optional and has default values. Now, you can override pretty much any aspect of the Spinner right here, pass in a parameter ShowLabel = false and the 'group label' simply won't be shown. Pass in a Size property of 0,100 and the whole control will be 100 px tall and as wide as the content requires.
There are two Local commands created here. One for Next and Previous Choice values. These are the same commands that I mentioned previously (in the SpinnerButton UI). Looking at the Rules section here you can see I Bind the value of Choice.HasNextValue and HasPreviousValue to Command.Available - this is how the button knows to be enabled or disabled.
Again, looking at the Rules section, I Bind the Choice.Description value to LabelText.Content - this means you set a Description on your Choice and it's automatically displayed as a header to the whole control. Another rule, if Input.DeepKeyFocus = true sets the color of said Label to White.
You'll notice there are a few extra properties in the code above, that are not used in any of the actual UI's - that's because this is taken directly out of my myTV application, and I'm using some of these UI's as Bases for other UI's - not wanting to spend too much time on this I left things pretty much as they are - You're free to experiment with the code of course and remove/add properties as you see fit.
Right, so how do you use this control?
You call it from your parent UI by doing something like this;
<
spinner:TextSpinner Name="ShowOrderSpinner" Choice="[App.ShowOrderBy]"/>
<comm:Divider.Horizontal/>
<spinner:TextSpinner Name="EpisodeOrderSpinner" Choice="[App.EpisodeOrderBy]"/>
again, this is taken directly from myTV, but it shows you the Choice (ShowOrderBy and EpisodeOrderBy) is created in App. Which is the Application object passed to all my main UIs. Here's an example of this, taken out of context, but should give you the basics;
private Choice _showOrderBy;
[MarkupVisible]
internal Choice ShowOrderBy
{
get { return _showOrderBy; }
set { if (_showOrderBy != value) { _showOrderBy = value; base.FirePropertyChanged("ShowOrderBy"); } }
}
[MarkupVisible]
internal void LoadSettingsChoices()
{
al = new ArrayListDataSet();
al.Add("Show UID");
al.Add("Show Name");
al.Add("Status");
al.Add("First Aired");
al.Add("Network");
al.Add("Genre");
al.Add("Last Viewed");
al.Add("Airs Day of Week");
al.Add("Airs Time");
al.Add("Rating");
al.Add("Location");
_showOrderBy = new Choice(this.Owner, "Order shows by", al);
_showOrderBy.ChosenIndex = _showOrderBy.Options.IndexOf(myTVSettingsStore.Settings.ShowOrderBy);
_showOrderBy.ChosenChanged += new EventHandler(EnableSave);
}
Things to note here are; I set everything as internal or private. I do this to stop other applications from accessing my internal code. Do note you need to include [MarkupVisible] to have access to it from your MCML code though.
new Choice(this.Owner, "Order shows by", al). This creates _showOrderBy as a new Choice, sets the description to "Order shows by" and fills it with the objects from al.
.ChosenIndex = ... this set's the default chosen item in the Choice to whatever is in myTVSettingsStore.Settings.ShowOrderBy (which is a public string).
.ChosenChanged += ... creates a new EventHandler, so whenever the selected item is changed (by pressing the + or - button) it fires EnableSave.
And there you go. All ready to use in your own application. Any questions, comment here, post over at the mediacenter sandbox or pop me an email. Enjoy.
Posted
Sun, Nov 18 2007 16:12
by
admin