twinbasic / lang-design

Language Design for twinBASIC
MIT License
11 stars 1 forks source link

Better support for embedded resources #29

Open WaynePhillipsEA opened 3 years ago

WaynePhillipsEA commented 3 years ago

Is your feature request related to a problem? Please describe.

VB6 doesn't offer a particularly nice way of working with embedded resources. We have LoadResData (etc), but these functions take a simple string or ordinal identifier that is not verified until runtime.

Describe the solution you'd like

I'd like resources to be accessible directly via the object model (e.g. EmbeddedResources.BITMAP.MyFile, and for resource types to be enumerable (e.g. For Each Resource In EmbeddedResources.BITMAP). This would allow for compile time verification of resource use (and later allowing for rename refactoring etc).

Additional context

We'd need to accommodate dots inside the resource names (e.g. EmbeddedResources.BITMAP.[MyFile.bmp]) and resources that are identified by ordinals.

The object returned for each resource should provide appropriate methods. e.g. resources in the BITMAP folder would offer a ToPicture() method that maps to the legacy LoadResPicture function.

Kr00l commented 3 years ago

Good idea. I would even simplify EmbeddedResources into just Resources. Because resources are always embedded, or not? So an enumeration could be

Dim Res As Resource
For Each Res In Resources
   If Res.Type = vbResBitmap Then
   ...
   End If
Next Res

And/or alternatively (can be in addition to above scenario which enums technically all resources)

Dim Res As Resource
For Each Res In Resources.Bitmaps
...
Next Res
Kr00l commented 3 years ago

Addition: The Resources.Bitmaps could be an collection which can be accessed by an index (one-based in order it is displayed in the VS code folder in IDE) or key (= MyFile.bmp)

WaynePhillipsEA commented 3 years ago

It might also be worth supporting BCP47 language tags (e.g. en-GB and en-US) as well as the current raw LCID_xxxx format for localization support.

bclothier commented 3 years ago

I have a possibly ignorant question. For graphic resources, it seems to me that VB* and C++ is very much based on raster images, which make it a poor fit for designing responsive forms or handling high DPI.

We obviously have to support raster images such as bitmaps for backward compatibility. However, I'm thinking that we should have support for vector images to allow seamless scaling, or at least make it easy to provide a larger raster image that can scale down reasonably well. The point here is we want to encourage moving away from fixed-size resources which won't work with large range of scale.

bclothier commented 3 years ago

One major shortcoming with VBx is that it does not make it easy to create & maintain a resource table that's more complicated than just a bunch of strings. We already have the ability to create string tables. However, I think we can do better than that.

Custom errors:

It's not just a string. We usually have those properties:

Not all of those need to be populated; some of them (e.g. Source or HelpFile might be either hard-coded or calculated by an expression. For example, CurrentComponentName & "." & CurrentProcedureName might be a good candidate for the Source. Likewise, Number might be calculated as vbObjectError + N .

Because of the above, it can get awkward trying to formulate a Err.Raise. In some case, we may have something that looks like this:

Public Enum MyErrorCodes
  SomeComplicatedErrorCode = vbObjectError + 1
  ...
End Enum

Public Sub ReportSomeComplicatedError( _
  SomeData As String, _
  ComponentName As String = CurrentComponentName, _
  ProcedureName As String = CurrentProcedureName) _
)
  Dim AdditionalInfo As String
  AdditionalInfo = GetMoreInfo()
  Err.Raise SomeComplicatedErrorCode, ComponentName & "." & ProcedureName, "Yikes, something went wrong with the " & SomeData & "..." & AdditionalInfo
End Sub

This is a maintenance nightmare because we now have an enum of error codes, then we have a procedure that is specialized to build an error for this one error. We'd need to write other procedures -- that can get very tedious. Then we have to call the specialized procedure and pass in the arguments to provide the context needed from the procedure where the error should be raised. Way too painful.

Custom messages

Messages are another example where it's not just a string. For example, we may want to use a certain title or a certain icons/buttons associated with a message. We might want to be able to do something like this:

If vbYes = PromptUserForConfirm() Then

Normally, we'd have to write a procedure, maybe something like this:

Public Function PromptUserForConfirm() As vbMsgBoxResult
  Return MsgBox("Are you sure?", vbExclamation Or vbYesNo, "Confirm")
End Function

Note that we have 2 strings and a enum to govern the formatting of the messagebox. With a string table, we'd need to create 2 entries for the message text and the title, which is more maintenance.

Strongly typing the resources

In .NET, when a resource is created, we can have a early-bound & intellisense aware of resources (e.g. Resources.MyCustomMessage. That does help a lot with finding and tracking the usages of resources in the codebase. Therefore, the message box example above could be something like:

MsgBox(Resource.ConfirmUser_Text, vbExclamation Or vbYesNo, Resources.ConfirmUser_Title)

This is better now that we no longer have literals in the code and we can at least find the 2 related resources.

But can we do better? I think yes. I can see 2 ways:

Structure-based table

Instead of an ordinary string, we can allow for a structure to act as a record. Something like this as an example:

ErrorData = {
  Number: Long,
  Description: String
},
[
  {
    Id = "ErrorOne",
    Number = 1,
    Description = "Error One"
  },
  {
    Id = "ErrorTwo",
    Number = 2,
    Description = "Error Two"
  }
]

We can then make a generic helper function:

Public Sub ReportError( _
  Data As ErrorData, _
  ComponentName As String = CurrentComponentName, _
  ProcedureName As String = CurrentProcedureName, _
)
  Err.Raise Data.Number + vbObjectError, ComponentName & "." & ProcedureName, Data.Description
End Sub

The calling code would then be something like:

ReportError ErrorDataResources.ErrorOne

Note that the ErrorDataResources would be an automatically generated class based on the structure, returning a ErrorData structure and creating a member for each Id entry. No literals anywhere!

You can get similar idea for the custom messaging and other possible uses, I think.

However, there's one more way we can improve!

Expression-based table

In the above example, we were still using literals which we need to then call into a helper function to construct everything we need. That also means that if we need a different format, we'd need to add branching in the helper function or create a different helper functions. That can get messy...

What if we could do this instead:

ErrorData = {
  Parameters: {
    ComponentName = CurrentComponentName,
    ProcedureName = CurrentProcedureName
  },
  BaseNumber: Long,
  ErrorNumber: '=BaseNumber + vbObjectError'
  Source: '=ComponentName & "." & ProcedureName'
  Description: String
},
[
  {
    Id = "ErrorOne",
    BaseNumber = 1,
    Description = "Error One: Welp...."
  },
  {
    Id = "ErrorTwo",
    Parameters = {
      AdditionalData: String
    },
    Number = 2,
    Description = "Error Two: {FormatAdditionalErrorInformation(AdditionalData)}"
  }
]

The calling codes for each would then be like so:

ReportError ErrorDataResources.ErrorOne
ReportError ErrorDataResources.ErrorTwo(Data)

This way, we do not need to customize ReportError with multiple versions and compiler can verify that the ErrorTwo has the required parameter AdditionalData provided by the calling code, separate from the ErrorOne which has no required parameters.

Because we can write expressions, I think that yields for a more readable and easier to understand system than if we had to write several overloaded functions and remembering to supply the right parameters for right error/message/whatever it is and hope that we did call the right function...