Use command bindings for the save button, then you can enable/disable the button depending on your current state
see this simple tutorial and if you want further explantion the msdn article, also josh smith gets more in depth
We handle your above situation by using a combination of commands and an IsValid property on the underlying model we are binding to. We do validation at the business model level (some times in the ui as well) and when the business model is valid we enable the command, or as in your case, save button.
Here is a sample of the style we apply to our text boxes (we derive from text box and give it another property called SimpleField. This field has the properties IsValid, IsDirty, IsReadOnly, ErrorMessage and DatabaseValue. This enables us to know if the field is valid, whether it has changed if it is read only (i.e. the user doesnt have the permission to change the value or it is locked for another reason), if there is an error message (associated with the IsValid property) and also the database value (for when the field has changed, the user can see the original value) We use all of these properties in the style below
<!-- Simple TextBox -->
<Style
TargetType="{x:Type local:SimpleFieldTextBox}"
BasedOn="{StaticResource {x:Type TextBox}}">
<Setter
Property="KeyboardNavigation.TabNavigation"
Value="None" />
<Setter
Property="FocusVisualStyle"
Value="{x:Null}" />
<Setter
Property="AllowDrop"
Value="True" />
<Setter
Property="SnapsToDevicePixels"
Value="True" />
<Setter
Property="Height"
Value="22" />
<Setter
Property="Template">
<Setter.Value>
<ControlTemplate
TargetType="{x:Type local:SimpleFieldTextBox}">
<Border
x:Name="PART_SimpleFieldTextBox"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Height="{TemplateBinding Height}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="Auto" />
<ColumnDefinition
Width="*" />
</Grid.ColumnDefinitions>
<!-- The implementation places the Content into the ScrollViewer.
It must be named PART_ContentHost for the control to function -->
<ScrollViewer
x:Name="PART_ContentHost"
Grid.Column="1"
Margin="0" />
<!-- Not Valid Icon -->
<Path
x:Name="IconError"
Grid.Column="0"
Fill="Red"
Stretch="Fill"
Margin="1,1,4,1"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Visibility="Collapsed"
Width="4"
Height="14"
SnapsToDevicePixels="True"
Data="M0,11 L6,11 6,14 0,14 z M0,0 L6,0 6,10 0,10 z">
<Path.ToolTip>
<ToolTip>
<StackPanel
Orientation="Vertical"
MaxWidth="300"
MaxHeight="100">
<TextBlock
FontStyle="Italic"
Text="Error:" />
<TextBlock
Margin="8,0,0,0"
TextWrapping="WrapWithOverflow"
TextTrimming="CharacterEllipsis"
Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=SimpleField.ErrorMessage}" />
<TextBlock
Margin="0,4,0,0"
FontStyle="Italic"
Text="Original Value: " />
<TextBlock
Margin="8,0,0,0"
TextWrapping="WrapWithOverflow"
TextTrimming="CharacterEllipsis"
Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=SimpleField.DatabaseValue}" />
</StackPanel>
</ToolTip>
</Path.ToolTip>
</Path>
<!-- Valid (but changed) Icon-->
<Path
x:Name="IconWarning"
Grid.Column="0"
Fill="#FF5BBD30"
Stretch="Fill"
Margin="1,1,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Visibility="Collapsed"
Width="8"
Height="8"
SnapsToDevicePixels="True"
Data="M0,0 L8,0 0,8 z">
<Path.ToolTip>
<ToolTip>
<StackPanel
Orientation="Vertical"
MaxWidth="500"
MaxHeight="100">
<TextBlock
Text="Original Value: " />
<TextBlock
Margin="8,0,0,0"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=SimpleField.DatabaseValue}" />
</StackPanel>
</ToolTip>
</Path.ToolTip>
</Path>
</Grid>
</Border>
<ControlTemplate.Triggers>
<!-- Stop the text box being edited if the simple field is read only -->
<DataTrigger
Binding="{Binding RelativeSource={RelativeSource Self}, Path=SimpleField.IsReadOnly}"
Value="True">
<Setter
Property="IsReadOnly"
Value="True" />
<Setter
Property="Foreground"
Value="{StaticResource DisabledForegroundBrush}" />
<Setter
TargetName="PART_SimpleFieldTextBox"
Property="Background"
Value="{StaticResource DisabledBackgroundBrush}" />
<Setter
TargetName="PART_SimpleFieldTextBox"
Property="BorderBrush"
Value="{StaticResource DisabledBorderBrush}" />
</DataTrigger>
<!-- IsEnabled condition -->
<DataTrigger
Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsEnabled}"
Value="False">
<Setter
Property="Foreground"
Value="{StaticResource DisabledForegroundBrush}" />
<Setter
TargetName="PART_SimpleFieldTextBox"
Property="Background"
Value="{StaticResource DisabledBackgroundBrush}" />
<Setter
TargetName="PART_SimpleFieldTextBox"
Property="BorderBrush"
Value="{StaticResource DisabledBorderBrush}" />
</DataTrigger>
<!-- When value inside field has been changed -->
<DataTrigger
Binding="{Binding RelativeSource={RelativeSource Self}, Path=SimpleField.IsDirty}"
Value="True">
<Setter
TargetName="IconWarning"
Property="Visibility"
Value="Visible" />
</DataTrigger>
<!-- When value inside field is NOT valid -->
<DataTrigger
Binding="{Binding RelativeSource={RelativeSource Self}, Path=SimpleField.IsValid}"
Value="False">
<Setter
TargetName="IconWarning"
Property="Visibility"
Value="Collapsed" />
<Setter
TargetName="IconError"
Property="Visibility"
Value="Visible" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>