This seems to do the trick. What I do is use a template column instead of a plain text column. Now I can swap templates for each individual cell based on properties to which that row in the grid are bound. So I then simply have one editable version of the template and one that is read-only.
<DataGrid ItemsSource="{Binding CostItems}"
HeadersVisibility="Column"
SelectionUnit="Cell"
SelectionMode="Single">
<DataGrid.Resources>
<DataTemplate x:Key="UnitCostAllowModifyTemplate">
<TextBox IsReadOnly="False" IsTabStop="True">
<Binding Path="UnitCost" UpdateSourceTrigger="LostFocus"/>
</TextBox>
</DataTemplate>
<DataTemplate x:Key="UnitCostBlockModifyTemplate">
<TextBox IsReadOnly="True" IsTabStop="False">
<Binding Path="UnitCost"/>
</TextBox>
</DataTemplate>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding={Binding Name}"/>
<DataGridTemplateColumn Header="Unit Cost">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentControl Content="{Binding .}">
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}">
<Setter Property="KeyboardNavigation.IsTabStop" Value="False"/>
<Setter Property="ContentTemplate" Value="{StaticResource UnitCostAllowModifyTemplate}" />
<Style.Triggers>
<DataTrigger Binding="{Binding UnitCostFixed}" Value="False">
<Setter Property="ContentTemplate" Value="{StaticResource UnitCostBlockModifyTemplate}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
Top Tip I found on the internet is to use a dummy converter on any binding you can't debug. Then you can break in the debugger and see exactly what object the binding is getting.