C# Corner
The Bridge Pattern in the .NET Framework
Learn how to implement the Bridge Pattern in .NET by building a Windows Store radio application.
The Bridge Pattern is a common software design pattern that allows a class's implementation to be decoupled from its abstraction so the two can vary independently of one another. Today I'll cover the core components of the pattern and go over how to put it to use in a sample application.
The pattern consists of four components:
- Abstraction
- Implementor
- Refined abstraction
- One or many concrete implementors
The implementor is an interface that defines base functionality for all concrete impelementors. The concrete implementor class or classes implement the implementor interface. The abstraction class provides an interface to the client application independent of the concrete implementor being used. The refined abstraction class extends the abstraction interface.
Now that you understand the core components of the Bridge Pattern, let's look at a concrete example. The sample program will let the user control a car radio that has both an FM and an XM tuner. Once a tuner's selected, the user may tune to a specific station and flip to the next, or previous, station.
To get started, create a new C# Windows Store App in Visual Studio 2012 or 2013. The first thing to add is the implementor interface for a radio tuner named IRadioTuner. The IRadioTuner interface contains properties for getting the current station's information, the delta between stations and a method for setting the current station as a floating point number:
namespace VSMBridgePattern
{
public interface IRadioTuner
{
string StationInfo { get; }
float StationDelta { get; }
void SetStation(float station);
}
}
Next I add the concrete implementor classes for the FM and XM tuners that implement the IRadioTuner interface. The FmTuner class implements the IRadioTuner interface and sets the StationDelta to 0.1 in its constructor:
public string StationInfo { get; private set; }
public float CurrentStation { get; set; }
public float StationDelta { get; private set; }
public FmTuner()
{
StationDelta = 0.1f;
}
The SetStation method sets the CurrentStation to the given station value rounding the value to nearest tenth and sets the StationInfo to FM X, where X is the rounded station value:
public void SetStation(float station)
{
CurrentStation = (float)Math.Round(station, 1);
StationInfo = string.Format("{0} FM", CurrentStation);
}
The completed FmTuner class implemention:
using System;
namespace VSMBridgePattern
{
public class FmTuner : IRadioTuner
{
public string StationInfo { get; private set; }
public float CurrentStation { get; set; }
public float StationDelta { get; private set; }
public FmTuner()
{
StationDelta = 0.1f;
}
public void SetStation(float station)
{
CurrentStation = (float)Math.Round(station, 1);
StationInfo = string.Format("{0} FM", CurrentStation);
}
}
}
Next, I add the XmTuner class that implements the IRadioTuner interface. First I set the StationDelta to 1 in the constructor and define the needed properties:
public string StationInfo { get; private set; }
public float StationDelta { get; private set; }
public float CurrentStation { get; private set; }
public XmTuner()
{
StationDelta = 1;
}
In the SetStation method, I set the CurrentStation to the integer rounded value of the given station, because XM stations doesn't have fractions. Then I set the StationInfo to XM Y, where Y is the rounded station value:
public void SetStation(float station)
{
CurrentStation = (int)station;
StationInfo = string.Format("XM {0}", CurrentStation);
}
Here's a look at the completed XmTuner class implementation:
namespace VSMBridgePattern
{
public class XmTuner : IRadioTuner
{
public string StationInfo { get; private set; }
public float StationDelta { get; private set; }
public float CurrentStation { get; private set; }
public XmTuner()
{
StationDelta = 1;
}
public void SetStation(float station)
{
CurrentStation = (int)station;
StationInfo = string.Format("XM {0}", CurrentStation);
}
}
}
Next I implement the Radio abstraction class that utilizes a set IRadioTuner instance to control a radio object. First I define the IRadioTuner property named RadioTuner and its protected _current channel member variable in the Radio class:
public IRadioTuner RadioTuner { get; set; }
protected float _currentChannel;
Then I add the RadioStatus, PowerStatus and Enabled properties to the class, which will be set in the refined abstraction class:
public string RadioStatus { get; protected set; }
public string PowerStatus { get; protected set; }
public bool Enabled { get; protected set; }
Then I add abstract On and Off methods that are overridden in the refined abstraction class:
public abstract void On();
public abstract void Off();
Next I add the TuneToChannel method that tunes the current radio tuner to the given station, and stores that value into the stored current channel:
public virtual void TuneToChannel(float channel)
{
RadioTuner.SetStation(channel);
_currentChannel = channel;
}
Then I add the NextChannel method that increments the current channel by the radio tuner's station delta and tunes to new station:
public virtual void NextChannel()
{
_currentChannel += RadioTuner.StationDelta;
TuneToChannel(_currentChannel);
}
Next I add the PreviousChannel method that decrements the current channel by the radio tuner's station delta and tunes to the new station:
public virtual void PreviousChannel()
{
_currentChannel -= RadioTuner.StationDelta;
TuneToChannel(_currentChannel);
}
The completed Radio class implementation:
namespace VSMBridgePattern
{
public abstract class Radio
{
public bool Enabled { get; protected set; }
public IRadioTuner RadioTuner { get; set; }
public string RadioStatus { get; protected set; }
public string PowerStatus { get; protected set; }
protected float _currentChannel;
public abstract void On();
public abstract void Off();
public virtual void TuneToChannel(float channel)
{
RadioTuner.SetStation(channel);
_currentChannel = channel;
}
public virtual void NextChannel()
{
_currentChannel += RadioTuner.StationDelta;
TuneToChannel(_currentChannel);
}
public virtual void PreviousChannel()
{
_currentChannel -= RadioTuner.StationDelta;
TuneToChannel(_currentChannel);
}
}
}
Now it's time to impelemnt the CarRadio refined abstraction class, which is the last piece of the Bridge Pattern implementation. The CarRadio class inherits from the Radio abstraction class, and implements the INotifyPropertyChanged interface:
public class CarRadio : Radio, INotifyPropertyChanged
First, I add the RadioStatus property to the CarRadio class. The RadioStatus property calls the NotifyPropertyChanged method to notify any subscribers that the property value has changed in its setter:
private string _radioStatus;
public override string RadioStatus
{
get { return _radioStatus; }
protected set
{
if (_radioStatus == value) return;
_radioStatus = value;
NotifyPropertyChanged();
}
}
Then I add the PowerStatus property that sets a backing field and calls the NotifyPropertyChanged method to notify subscribers that the property value has changed in its setter:
private string _powerStatus;
public override string PowerStatus
{
get { return _powerStatus; }
protected set
{
if (_powerStatus == value) return;
_powerStatus = value;
NotifyPropertyChanged();
}
}
Next I add the On method that sets the Enabled property to true and sets the PowerStatus description to "Welcome":
public override void On()
{
Enabled = true;
PowerStatus = "Welcome";
}
Then I add the Off method that sets the Enabled property to false and sets the PowerStatus description to "Goodbye":
public override void Off()
{
Enabled = false;
PowerStatus = "Goodbye";
}
Next I add the TuneToChannel method that calls the base Radio method to tune the station, and sets the RadioStatus to the StationInfo property value on the RadioTuner:
public override void TuneToChannel(float channel)
{
base.TuneToChannel(channel);
RadioStatus = RadioTuner.StationInfo;
}
The last step is to implement the INotifyPropertyChanged related event and NotifyPropertyChanged method used by the RadioStatus and PowerStatus properties:
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
The CarRadio class is now complete:
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace VSMBridgePattern
{
public class CarRadio : Radio, INotifyPropertyChanged
{
private string _radioStatus;
public override string RadioStatus
{
get { return _radioStatus; }
protected set
{
if (_radioStatus == value) return;
_radioStatus = value;
NotifyPropertyChanged();
}
}
private string _powerStatus;
public override string PowerStatus
{
get { return _powerStatus; }
protected set
{
if (_powerStatus == value) return;
_powerStatus = value;
NotifyPropertyChanged();
}
}
public override void On()
{
Enabled = true;
PowerStatus = "Welcome";
}
public override void Off()
{
Enabled = false;
PowerStatus = "Goodbye";
}
public override void TuneToChannel(float channel)
{
base.TuneToChannel(channel);
RadioStatus = RadioTuner.StationInfo;
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
Now it's time to tie everything together. Open up the MainPage.xaml file and copy the root <Grid> element's content from this:
<Page
x:Class="VSMBridgePattern.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:VSMBridgePattern"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
<StackPanel Name="PowerDetails" Margin="20">
<Button Name="On" Click="On_Click">On</Button>
<Button Name="Off" Click="Off_Click">Off</Button>
<TextBlock Name="PowerStatus" Text="{Binding PowerStatus, Mode=TwoWay}" MinHeight="20" MinWidth="100"></TextBlock>
<StackPanel Name="RadioDetails" Visibility="Collapsed">
<TextBlock Name="RadioStatus" Text="{Binding RadioStatus, Mode=TwoWay}" MinHeight="20" MinWidth="100"></TextBlock>
<TextBlock>Tuner</TextBlock>
<ComboBox Name="Stations" ItemsSource="{Binding}" DisplayMemberPath="Key" SelectedValuePath="Value" SelectionChanged="Stations_SelectionChanged"></ComboBox>
<TextBlock>Go to Station</TextBlock>
<TextBox Name="Station"></TextBox>
<Button Name="Tune" Click="Tune_Click">Tune</Button>
<Button Name="Previous" Click="Previous_Click">Previous</Button>
<Button Name="Next" Click="Next_Click">Next</Button>
</StackPanel>
</StackPanel>
</Grid>
</Page>
The last step is to update the MainPage code-behind. First I add a Radio member variable named _radio, initialize it in the constructor and bind it to the RadioDetails and PowerDetails StackPanel elements:
private readonly Radio _radio;
public MainPage()
{
this.InitializeComponent();
_radio = new CarRadio();
RadioDetails.DataContext = _radio;
PowerDetails.DataContext = _radio;
}
Then I add the LoadTuners method, which populates the Stations ComboBox with the FM and XM tuner options:
private void LoadTuners()
{
var tuners = new Dictionary<string, IRadioTuner>()
{
{ "FM", new FmTuner()},
{"XM", new XmTuner()}
};
Stations.ItemsSource = tuners;
Stations.SelectedIndex = 0;
}
Then I call the LoadTuners method in the OnNavigatedTo method:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
LoadTuners();
}
Next I handle the On button Click event by turning on the radio and displaying the RadioDetails StackPanel:
private void On_Click(object sender, RoutedEventArgs e)
{
_radio.On();
RadioDetails.Visibility = Visibility.Visible;
}
Then I handle the Off button Click event by turning off the radio and collapsing the RadioDetails StackPanel:
private void Off_Click(object sender, RoutedEventArgs e)
{
_radio.Off();
RadioDetails.Visibility = Visibility.Collapsed;
}
Next I handle the Tune button Click event by calling the TuneToChannel method on the radio, passing in the user's entered station value:
private void Tune_Click(object sender, RoutedEventArgs e)
{
if(!String.IsNullOrWhiteSpace(Station.Text))
_radio.TuneToChannel(float.Parse(Station.Text));
}
Then I set the radio's tuner to the selected Stations ComboBox value in the Stations' SelectionChanged event handle:
private void Stations_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
_radio.RadioTuner = Stations.SelectedValue as IRadioTuner;
}
Next, I handle the Next button Click event by calling the NextChannel method on the radio:
private void Next_Click(object sender, RoutedEventArgs e)
{
_radio.NextChannel();
}
Finally, I handle the Previous button Click event by calling the PreviousChannel method on the radio:
private void Previous_Click(object sender, RoutedEventArgs e)
{
_radio.PreviousChannel();
}
Here's how the completed MainPage class implementation should look:
using System;
using System.Collections.Generic;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
// The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=234238
namespace VSMBridgePattern
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
private readonly Radio _radio;
public MainPage()
{
this.InitializeComponent();
_radio = new CarRadio();
RadioDetails.DataContext = _radio;
PowerDetails.DataContext = _radio;
}
private void LoadTuners()
{
var tuners = new Dictionary<string, IRadioTuner>()
{
{ "FM", new FmTuner()},
{"XM", new XmTuner()}
};
Stations.ItemsSource = tuners;
Stations.SelectedIndex = 0;
}
/// <summary>
/// Invoked when this page is about to be displayed in a Frame.
/// </summary>
/// <param name="e">Event data that describes how this page was reached. The Parameter
/// property is typically used to configure the page.</param>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
LoadTuners();
}
private void On_Click(object sender, RoutedEventArgs e)
{
_radio.On();
RadioDetails.Visibility = Visibility.Visible;
}
private void Off_Click(object sender, RoutedEventArgs e)
{
_radio.Off();
RadioDetails.Visibility = Visibility.Collapsed;
}
private void Tune_Click(object sender, RoutedEventArgs e)
{
if(!String.IsNullOrWhiteSpace(Station.Text))
_radio.TuneToChannel(float.Parse(Station.Text));
}
private void Stations_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
_radio.RadioTuner = Stations.SelectedValue as IRadioTuner;
}
private void Next_Click(object sender, RoutedEventArgs e)
{
_radio.NextChannel();
}
private void Previous_Click(object sender, RoutedEventArgs e)
{
_radio.PreviousChannel();
}
}
}
The application is now complete; when you start it up you should have the option of turning on the radio, as seen in Figure 1.
Next you should be able to turn on the radio and tune to a station, as seen in Figure 2.
And last, you should be able to turn of the radio and see the "Goodbye" message from Figure 3.
The Bridge Pattern is an effective design pattern when you want to have multiple implementations of a constantly changing interface. The pattern emphasizes the SOLID design principles and leads to composition over inheritance. On the outside, it seems very similar to the Adapter Pattern, but its main difference is that the Bridge Pattern is used to make things work before a class hierarchy is designed; the Adapter Pattern is used after things have been implemented.