Getting fine level control of serial comms on modern PCs is pretty hard, because there is now so much OS in the way. Trying to read bytes one at a time will not be any more reliable than reading chunks, and generally just uses up CPU.
A ReceiveString function will never return whole packets, because it doesn't know the packet format.
I think it is best to handle the data arriving in chunks, and accept the fact that chunks will come in every combination possible, i.e. start, middle, end, plus mixed with last/next packets and also errors or other junk.
However, if you write the code with a routine called process_rx_byte, which handles bytes one at a time, identifies whole packets, and discards faulty ones, all of which you have to do anyway, then you can call the same routine from an OnCharReceived event or an OnStringReceived.
Receiving data as bytes or string is functionally equivalent.