Contents
Learn how to create a Calcium project using the new Calcium SDK in this getting started video. Please note that clicking on the image will take you to the Calcium SDK website, where you can watch the video. I wanted to show it inline with the article, however it proved to be too difficult.
Introduction
Calcium is a WPF composite application toolset that leverages the Composite Application Library. It provides much of what one needs to rapidly build a multifaceted and sophisticated modular application.
Figure: Calcium Desktop Environment
In part one of this series, we explored some of Calcium’s core infrastructure, including module management, region adaptation, and the Bootstrapper
. In part two we looked at the location agnostic message service, which allows a set of common dialogs to be displayed to the user from anywhere, be it server-side or client-side; the web browser module, and the Output module. Now we will examine the View Service and File Service, and discuss various changes to the codebase since writing the first article.
In the first article we saw that Calcium consists of a client application and server based WCF services, which allow interaction and communication between clients. Out of the box, Calcium comes with a host of modules and services, and an infrastructure that is ready to use in your next application.
There are many parts to Calcium, and for that reason I’ve decided to break this article up into a series of several articles.
- Introduction to Calcium, Module Manager.
- Message Service, Web Browser module, Output Module.
- View Service, File Service, Rebranding Calcium (this article)
- Text Editor Module, User Affinity Module, Communication Module
- TBA
In this article you will learn how to:
- Create a Calcium project using the Calcium SDK (Video),
- use a file service to abstract the saving and loading of data from any source,
- and show or hide content using a type identification system.
Some of the content in this series of articles are not at an advanced level and will suite even those that are new to Prism. While others, such as the messaging system, will suite more advanced readers. Hopefully there are things here to learn for everyone.
These series of articles are, in some respects, an introduction to some areas of Prism. Although, if you are completely new to Prism, you may find yourself somewhat overloaded at times, and I would recommend taking a look at some beginner Prism articles before you tackle Calcium.
Since Last Time
Since last time there have been numerous enhancements that have been made based on feedback I have received from Calcium users. Most notably, now Calcium is able to be completely rebranded. This includes replacing or customizing the: Splash Screen, About Box, Logo, and shell Title text. I have also gone to considerable effort to create VS templates for Visual Studio 2008 and 2010, including support for both C# and VB.NET.
New Visual Studio Project Templates
I have created a set of twelve Calcium project templates. There are three core templates: Launcher project, Module project, and Web Application project, which have been ported for different environments: Visual Studio 2008 (C# and VB.NET), and Visual Studio 2010 (C# and VB.NET).
Calcium Module Project Template
The Calcium Module project is perhaps the more interesting of the set of Calcium project templates. This template generates a set of three classes providing for a Prism module. When you create a new Calcium Module the project naming convention [Namespace].[ModuleName] is used to derive the names for the various generated items. For example, if you happen to name your new project MyNamespace.Demo, then what results is:
- a module called
MyNamespace.DemoModule
,
- a view called
MyNamespace.DemoView
,
- and a viewmodel called
MyNamespace.DemoViewModel
.
Behind the Scenes with Custom Template Parameters
To accomplish the automatic naming of project items an IWizard
implementation named ModuleProjectWizard
is used to insert a custom template parameter, which is then used to name our project output items. All of the Calcium SDK VS Templates make use of an IW
izard
, which is declared in each VS Template file like so:
<VSTemplate Version="2.0.0" Type="Project">
<TemplateData>
…
</TemplateData>
<TemplateContent>
…
</TemplateContent>
<WizardExtension>
<Assembly>DanielVaughan.Calcium.VSIntegration,
Version=1.0.1.0, Culture=Neutral, PublicKeyToken=f32f1bf552288cd5</Assembly>
<FullClassName>DanielVaughan.Calcium.VSIntegration.ModuleProjectWizard</FullClassName>
</WizardExtension>
</VSTemplate>
The assembly containing the ModuleProjectWizard
is placed in the gac during installation of the Calcium SDK. Subsequently, the ModuleProjectWizard
method, which is executed when a new Calcium Module project is created, looks like this:
public void RunStarted(object automationObject,
Dictionary<string, string> replacementsDictionary,
WizardRunKind runKind, object[] customParams)
{
string vsVersion;
if (!replacementsDictionary.TryGetValue("$VSVersion$", out vsVersion))
{
MessageBox.Show("Problem determining Visual Studio Version. "
+ "Please reinstall Calcium SDK.");
return;
}
if (runKind == WizardRunKind.AsNewProject
|| runKind == WizardRunKind.AsMultiProject)
{
string projectName = replacementsDictionary["$safeprojectname$"];
string lastWord = null;
int lastIndex = projectName.LastIndexOf('.');
if (lastIndex != -1 && lastIndex < projectName.Length - 1)
{
lastWord = projectName.Substring(lastIndex + 1);
}
if (string.IsNullOrEmpty(lastWord))
{
lastWord = "Custom";
}
replacementsDictionary["$CustomModule$"] = lastWord;
string installDir = RegistryUtil.GetCalciumInstallDirectory(vsVersion, "1.0");
replacementsDictionary["$InstallDir$"] = installDir;
}
}
The RunStarted
method is called by Visual Studio when the project is being created. We can see that the predefined $safeprojectname$
, which is in fact the name of the new project entered by the user (with any invalid characters removed), is split according to the naming convention mentioned earlier.
We are able to locate the location of the Calcium assemblies using the registry.
If you are wondering why I chose not to place the Calcium assemblies in the gac, it is because I wish to allow for customization using the source code as well as the SDK installer. As we know, the CLR looks in the gac first when resolving assemblies, so if assemblies were placed in the gac a developer would need to update the version numbers of all assemblies and config files. If you were just thinking “I didn’t know that the CLR looks in the gac first!”, then there is another reason for avoiding that approach, and I rest my case.
During installation of the Calcium SDK, we record the install path. We also use the Visual Studio version value located in the $VSVersion$
custom parameter to identify which registry key to use. The $VSVersion$
parameter is defined in each vstemplate like so:
<CustomParameters>
<CustomParameter Name="$VSVersion$" Value="9.0"/>
</CustomParameters>
So, we can see that custom parameters are able to be defined both in a vstemplate, and in code (in the IWizard
implementation).
Once a custom template parameter is defined, it can be consumed by a VS Template. In order to name project items using the custom parameter the VS Module Templates include the following excerpt:
C#
<ProjectItem ReplaceParameters="true"
TargetFileName="$CustomModule$Module.cs">TemplateModule.cs</ProjectItem>
<ProjectItem ReplaceParameters="true"
TargetFileName="$CustomModule$View.xaml">TemplateView.xaml</ProjectItem>
<ProjectItem ReplaceParameters="true"
TargetFileName="$CustomModule$View.xaml.cs">TemplateView.xaml.cs</ProjectItem>
<ProjectItem ReplaceParameters="true"
TargetFileName="$CustomModule$ViewModel.cs">TemplateViewModel.cs</ProjectItem>
VB.NET
<ProjectItem ReplaceParameters="true"
TargetFileName="$CustomModule$Module.vb">ModuleName1Module.vb</ProjectItem>
<ProjectItem ReplaceParameters="true"
TargetFileName="$CustomModule$View.xaml">ModuleName1View.xaml</ProjectItem>
<ProjectItem ReplaceParameters="true"
TargetFileName="$CustomModule$View.xaml.vb">ModuleName1View.xaml.vb</ProjectItem>
<ProjectItem ReplaceParameters="true"
TargetFileName="$CustomModule$ViewModel.vb">ModuleName1ViewModel.vb</ProjectItem>
The physical TemplateModule.cs file, for example, is copied to the newly created project, and its name is transformed from $CustomModule$Module.cs to DemoModule.cs.
Resulting Project Items
We’ve looked at what happens behind the scenes; now let’s turn our attention to the actual generated output. The content of the module is shown in the following excerpt:
C#
[Module(ModuleName = "Demo")]
public class DemoModule : ModuleBase
{
const string defaultRegion = RegionNames.Tools;
DemoView view;
public DemoModule()
{
Initialized += OnInitialized;
}
void OnInitialized(object sender, EventArgs e)
{
view = new DemoView();
var viewService = ServiceLocatorSingleton.Instance.GetInstance<IViewService>();
viewService.RegisterView("Demo", obj => ShowView(defaultRegion, view, true), null, null, null);
ShowView(defaultRegion, view, false);
}
}
VB.NET
<[Module](ModuleName:="Demo")> _
Public Class DemoModule
Inherits ModuleBase
Public Sub New()
AddHandler MyBase.Initialized, New EventHandler(AddressOf Me.OnInitialized)
End Sub
Private Sub OnInitialized(ByVal sender As Object, ByVal e As EventArgs)
Me.view = New DemoView
Dim viewService As IViewService = ServiceLocatorSingleton.Instance.GetInstance(Of IViewService)()
viewService.RegisterView("Demo", AddressOf ShowView2, Nothing, Nothing, Nothing)
ShowView2(Nothing)
End Sub
Private Sub ShowView2(ByVal obj As Object)
MyBase.ShowView(defaultRegion, Me.view, True)
End Sub
Private Const defaultRegion As String = "Tools"
Private view As DemoView
End Class
By default, the generated module subscribes to the base class Initialized
event. When the handler (OnInitialized
) is called, the module will instanciate a new view, and register it with the IViewService
. We cover view registration later in this article. Once the registration is complete, the module displays the new view, in the default region, using the base class ShowView
method.
The next project item that we will examine is the View. The following generated view XAML demonstrates how the DataContext
of the ViewControl
is set to a new instance of the DemoViewModel
.
C#
<Gui:ViewControl x:Class="CalciumWinProject1.Demo.DemoView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Gui="clr-namespace:DanielVaughan.Calcium.Client.Gui;assembly=DanielVaughan.Calcium.Client"
xmlns:Module="clr-namespace:CalciumWinProject1.Demo"
DataContext="{Binding ViewModel, RelativeSource={RelativeSource Self}}">
<Gui:ViewControl.ViewModel>
<Module:DemoViewModel />
</Gui:ViewControl.ViewModel>
<Grid>
</Grid>
</Gui:ViewControl>
VB.NET
The VB.NET template creates a nearly identical XAML file, apart from the x:Class
attribute, which does not include a namespace prefix.
<Gui:ViewControl x:Class="DemoView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Gui="clr-namespace:DanielVaughan.Calcium.Client.Gui;assembly=DanielVaughan.Calcium.Client"
xmlns:Module="clr-namespace:CalciumWinProjectVB.Demo"
DataContext="{Binding ViewModel, RelativeSource={RelativeSource Self}}">
<Gui:ViewControl.ViewModel>
<Module:DemoViewModel />
</Gui:ViewControl.ViewModel>
<Grid>
</Grid>
</Gui:ViewControl>
The generated DemoView.xaml.cs and DemoView.xaml.vb code-beside files, are absent of any presentation logic, View-ViewModel plumbing.
C#
public class DemoViewModel : ViewModelBase
{
public DemoViewModel()
{
TabHeader = "Demo";
}
}
VB.NET
Public Class DemoViewModel
Inherits ViewModelBase
Public Sub New()
MyBase.TabHeader = "Demo"
End Sub
End Class
The resultant ViewModel
derives from the ViewModelBase
class, and by convention the TabHeader
is set in the constructor.
Services Recap
Since the first release, the IoC infrastructure of Calcium has been overhauled. No longer are we explicit about the use of a Unity container, for this has been replaced with the Common Service Locator library, so that the IoC container can be substituted with a container of your choice. I still choose to use the Singleton pattern for service location, as I’ve found dependency injection to be troublesome when tracking down resolution failures. But feel free to use whatever approach suits you, e.g., DI via constructor injection. The ServiceLocatorSingleton
wraps the Microsoft.Practices.ServiceLocation.ServiceLocator
, so that only a reference to my base library is required.
The Microsoft.Practices.ServiceLocation.ServiceLocator
does not provide a means to register type associations. For that I have created a set of extension methods in the ServiceLocatorExtensions
class. In order to provide for registering of type associations, in an IoC container other than Unity, implement the DanielVaughan.ServiceLocationIServiceRegistrar
interface and register it with the container you wish to use.
public interface IServiceRegistrar
{
void RegisterInstance<TService>(TService service);
void RegisterInstance(Type serviceType, object service);
void RegisterType<TFrom, TTo>() where TTo : TFrom;
bool IsTypeRegistered(Type type);
}
The ServiceLocatorSingleton
will then make use of the type when registering type associations.
View Service
The IViewService
implementation allows for the association and interaction of UIElements
with workspace content, and provides the capability to have the visibility of a UIElement
change according to the visibility or active state of a view or view-model type.
Visibility Association
One feature of the IViewService
is that it allows one to hide or show any UI element in the shell, depending on the type of view that is active. When we associate a UIElement
, such as a ToolBar
, with a known type, if the current active view is of that type, then it will be visible, otherwise it will be hidden. We can see it in action in the Web Browser module, where we do just that: tell the service to hide the toolbar if the active view is an IWebBrowserView
.
var toolBarProvider = new WebBrowserToolBar { Url = startUrl };
var toolBar = toolBarProvider.ToolBar;
regionManager.Regions[RegionNames.StandardToolBarTray].Add(toolBar);
var viewService = UnitySingleton.Container.Resolve<IViewService>();
viewService.AssociateVisibility(typeof(IWebBrowserView),
new UIElementAdapter(toolBar), Visibility.Collapsed);
Here we have placed the toolbar in a WPF UserControl
called WebBrowserToolBar
so that we have designer support. We add the ToolBar
, which is exposed as a public property in the provider. Note it is necessary to remove the toolbar from its parent in the custom control, as a Visual Element can only have a single parent.
We could stop at this point and the toolbar would be always present in the shell. In order to hide and show the toolbar depending on the view however, we use the IViewService
instance. In particular, we use the AssociateVisibility
method to associate the type IWebBrowserView
, with the toolbar. The effect of this is to show the toolbar when the active view in the shell implements the IWebBrowserView
interface, otherwise the toolbar is hidden.
There are a couple of things to note here. First, we specify that the hidden state will be Collapsed
. This of course removes all trace of the item and allows other elements to occupy its space. It could alternatively be Hidden
, which would make the item invisible, yet retain the space taken by the element. This is not rocket science.
Second, we use a UIElementAdapter
to wrap the UIElement
. Why? Well in order to Mock System.Windows.UIElement
I thought it prudent to create a reusable wrapper and an associated interface IUIElement
. You see, UIElement.Visibility
is a dependency property, employing validation that prevents setting it arbitrarily. Thus we use a mockable interface and an adapter.
The following demonstrates how the IViewService
is tested using the IUIElement
interface.
[TestMethod]
public void ViewServiceShouldChangeVisibility()
{
var uiElement = MockRepository.GenerateStub<IUIElement>();
uiElement.Visibility = Visibility.Visible;
var viewService = UnityResolver.UnityContainer.Resolve<IViewService>();
viewService.AssociateVisibility(typeof(IViewDummyContent), uiElement, Visibility.Hidden);
var view = MockRepository.GenerateStub<IView>();
selectedDocumentViewChangedEvent.Publish(view);
Assert.AreEqual(Visibility.Hidden, uiElement.Visibility);
MockRepository mockRepository = new MockRepository();
var viewWithInterface = mockRepository.DynamicMultiMock<IView>(typeof(IViewDummyContent));
selectedDocumentViewChangedEvent.Publish(viewWithInterface);
Assert.AreEqual(Visibility.Visible, uiElement.Visibility);
}
View Registration
Another feature of the IViewService
is the ability to register a view, so that it is displayed in the View menu of the application. The Visual Studio module template automatically wires this up for you, as shown in the following excerpt, when you select Calcium Module from the Create New Project dialog within Visual Studio.
void OnInitialized(object sender, EventArgs e)
{
view = new DemoView();
var viewService = ServiceLocatorSingleton.Instance.GetInstance<IViewService>();
viewService.RegisterView("Demo", obj => ShowView(defaultRegion, view, true), null, null, null);
ShowView(defaultRegion, view, false);
}
The IViewService
has two overloads for the RegisterView method. The RegisterView
method is designed to accept a Func getDisplayName
, which should retrieve the text that is displayed in the view menu. There is also an Action that performs the opening of the view, so that when the view menu item is clicked, or the key gesture is performed, then the Action showView
will be performed.
Figure: Demo module automatically placed in the View menu after registration with the ViewService.
At the time of writing I am yet to implement the means to display the icon, so that serves as a placeholder for the time being. The default implementation of the IViewService
is the ViewService
, and the following is an excerpt from showing one of the view registration methods.
public void RegisterView(Func<object, object> getDisplayName,
Action<object> showView, object showViewParam,
KeyGesture showGesture, Func<object, object> getIcon)
{
var regionManager = ServiceLocatorSingleton.Instance.GetInstance<IRegionManager>();
IRegion viewRegion;
try
{
viewRegion = regionManager.Regions[MenuNames.View];
}
catch (KeyNotFoundException ex)
{
throw new UserMessageException(MenuNames.View
+ " region was not found.", ex);
}
var commandParameter = new OpenViewCommandParameter(showView, showViewParam);
var menuItem = new MenuItem
{
Command = ShowViewCommand,
Header = getDisplayName(showViewParam),
CommandParameter = commandParameter
};
viewRegion.Add(menuItem);
}
class OpenViewCommandParameter
{
public Action<object> EventAction { get; private set; }
public object Param { get; private set; }
public OpenViewCommandParameter(Action<object> eventAction, object param)
{
EventAction = eventAction;
Param = param;
}
}
File Service
At first glance the IFil
eService
interface appears to be merely an abstraction of OpenFileDialog
and SaveFileDialog
. It is, however, much more than that. While it does take care of displaying dialogs and handling common IO exceptions, in a platform agnostic manner. It also allows the user to select other alternative locations for files, and provides feedback to the user.
It is not the concern of the File Service to save or load files. Its only concern is in their selection. I suppose in some respects it could be called a File Selection Service. It is the task of the caller to specify a handler for saving or loading a file. This way we can have uniformity in dealing with common errors. I expect, in the future, to further abstract the dialog filter parameter, as it might be better to use a Known Association Manager for this.
We are able to be remarkably concise when using the File Service. For example, in the following excerpt we see how the TextEditorModule
is able to take over the common task of presenting a dialog. It will attempt to read the file and handle any IO Exceptions, notify the user, retry if necessary, and so on.
var fileService = ServiceLocatorSingleton.Instance.GetInstance<IFileService>();
string fileNameUsed = null;
string textFromFile = null;
FileOperationResult operationResult = fileService.Open(
name =>
{
textFromFile = File.ReadAllText(name);
fileNameUsed = name;
}, FileErrorAction.InformOnly, fileFilter);
if (operationResult == FileOperationResult.Successful)
{
}
The key to understanding this API is to recognize that it is the responsibility of the FileOperationHandler
to record the file name used. Once we have successfully opened or saved a file, the FileOperationHandler
should retain the file name that was used. The flexibility afforded to us by completely delegating the actual IO task etc., to the handler should not be underestimated. Kudos to Mike Krüger, previously of the #SharpDevelop project, from whom I first learned this technique, several years ago.
In other scenarios we are able to allow the user to select an alternate location for saving the file if the handler fails.
Diagram: File Service saves a file with error handling.
Diagram: File Service opens a file and shows dialogs automatically.
Rebranding Calcium
Previously rebranding Calcium (removing Calcium logos etc.) could only be done by modifying the source version. I have since provided for removing/replacing all trace of the Calcium branding from your application, without having to resort to getting one’s hands dirty.
Hiding the Calcium Banner Logo
Hiding the Calcium banner logo can be achieved by retrieving the IShell
instance from the service locator, and setting the LogoVisible
property to false, as shown in the following excerpt.
var shell = ServiceLocatorSingleton.Instance.GetInstance<IShell>();
shell.LogoVisible = false;
Populating the Banner Region
The Shell Banner is now a Prism region. This means you can retrieve it and populate it with UIElements
.
<Border x:Name="border_Banner" Grid.Row="0" Grid.ColumnSpan="3">
<Expander x:Name="expander_Banner" IsExpanded="True" ExpandDirection="Down"
Style="{DynamicResource ExpanderStyle}"
Expanded="Expander_Banner_ExpandChanged"
Collapsed="Expander_Banner_ExpandChanged"
Visibility="{Binding BannerVisible, Converter={StaticResource booleanToVisibilityConverter}}">
<StackPanel cal:RegionManager.RegionName="{x:Static Desktop:RegionNames.Banner}"
Orientation="Horizontal">
<Controls:TitleBanner x:Name="titleBanner" HorizontalAlignment="Stretch" Padding="0, 0, 10, 5"
Visibility="{Binding LogoVisible, Converter={StaticResource booleanToVisibilityConverter}}"/>
</StackPanel>
</Expander>
</Border>
To accomplish this I have created a custom Prism Region Adapter, called PanelRegionAdapter
. It is associated with the Panel type in the Bootstrapper
’s ConfigureRegionAdapterMappings
method, as shown in the following excerpt:
var mappings = base.ConfigureRegionAdapterMappings()
?? Container.Resolve<RegionAdapterMappings>();
mappings.RegisterMapping(typeof(Panel), Container.Resolve<PanelRegionAdapter>());
The StackPanel
is then populated using the region adapter.
Hiding the Entire Banner
If you wish to conserve space, and don’t wish to show the banner at all, you can hide it like so:
var mainWindow = ServiceLocatorSingleton.Instance.GetInstance<IMainWindow>();
mainWindow.BannerVisible = false;
Switching the About Box
The About Box can now be replaced by registering with the ServiceLocatorSingleton
an IAboutBox
type association.
ServiceLocatorSingleton.Instance.RegisterType<IAboutBox, MyAboutBox>();
Of course, MyAboutBox
must implement the IAboutBox
interface.
Changing the Window Title Text
Window title is now customizable, and also allows for string formatting. Thus we can register a localized string format once, and this will be applied whenever the Title is set. For example:
var mainWindow = ServiceLocatorSingleton.Instance.GetInstance<DanielVaughan.Gui.IMainWindow>();
mainWindow.Title = "project1";
mainWindow.TitleFormat = "{0} - My Application";
The way this works is that the title of the shell window is databound to its view model TabHeader
property.
Title="{Binding TabHeader}"
We set the StringFormat
property of the Title binding in code whenever the format changes. Here is an excerpt from the DesktopShellView
class:
void SetTitleFormat(string format)
{
Binding tabHeaderBinding = new Binding("TabHeader")
{
Source = DataContext,
StringFormat = format
};
SetBinding(TitleProperty, tabHeaderBinding);
ShellViewModel.TitleFormat = format;
}
Changing the Splash Screen Image
By using the StartupOptions
, the splash screen image can be replaced with an image that represents your product.
var starter = new AppStarter();
starter.StartupOptions.SplashImagePackUri
= new Uri("pack://application:,,,/YourAssembly;component/Resources/Images/YourImage.jpg");
starter.Start();
Hiding the Calcium Splash Screen Blurb
By default a piece of text is displayed in the bottom left hand corner of the splash screen. If you don’t like this, then you can remove it by, once again, using the StartupOptions
of the AppStarter
class, like so:
starter.StartupOptions.SplashBlurbVisible = false;
In a future release I will include a composite event that will allow a text field in the splash screen to display loading information to the user.
Changing the Web Browser Start URL
By default the Web Browser will attempt to load a default URL. To change this behaviour modify the app.config of your launcher application.
<appSettings>
<add key="WebBrowserStartUrl" value="http://www.example.com/"/>
</appSettings>
Calcium for Silverlight
Looking over the codebase, you may discover #if Silverlight preprocessor directives in various places. Yes, I am in the process of porting Calcium to Silverlight. It is my aim that most of the code in Calcium can target both the Desktop and Silverlight CLRs. So, a Module
, or a ViewModel
in a Calcium application can be used to target both Silverlight and WPF. Reaching this goal has been hindered by the absence of a Menu and Toolbar controls in Silverlight. I have had requests to provide for the Office 2007 Ribbon control in the WPF version, and serendipitously discovered that there is also a rather nice Silverlight implementation. So, depending on compatibility of the licensing of the Ribbon I may provide support for it on both platforms. We’ll see.
Conclusion
In this article we have looked at some of the changes that have taken place in the Calcium project since the first release, a number of months ago. We have seen how using Calcium has become even easier with the use of Custom Project Items, and we have explored behind the scenes of creating VS Templates for Calcium. We took a look at how the Service Location infrastructure has changed, and examined how to use the IViewService
to hide and show UIElements
. We also saw how the IFileService takes care of displaying dialogs and handling common IO exceptions, in a platform agnostic manner.
In the next article we will move to the Text Editor module and we’ll observe how the TextEditorViewModel
is able to provision for file changes using a content interface approach. We’ll also look at some of the other services including the Communication service and its related module.
As development of Calcium continues, I know we still have a lot to cover, and I hope you will join me for our next installment.
If you have a feature request, or discover a bug, please place it on the Calcium Codeplex Issue Tracker, or vote for the issue if it already exists.
I hope you find this project useful. If so, then I'd appreciate it if you would rate it and/or leave feedback below. This will help me to make my next article better.
History
November 2009